@wipcomputer/wip-ldm-os 0.4.85-alpha.2 → 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.
@@ -1,70 +1,320 @@
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 "package.json" "${APP_REMOTE_DIR}/package.json"
120
+ # Phone app static files (codex-remote-control, login).
121
+ if [ -d "${SCRIPT_DIR}/app" ]; then
122
+ while IFS= read -r f; do
123
+ rel=${f#${SCRIPT_DIR}/}
124
+ add_file "$rel" "${APP_REMOTE_DIR}/${rel}"
125
+ done < <(find "${SCRIPT_DIR}/app" -type f | sort)
126
+ fi
127
+ # Kaleidoscope demo files (login.html, index.html, agent.html, etc.).
128
+ if [ -d "${SCRIPT_DIR}/demo" ]; then
129
+ while IFS= read -r f; do
130
+ rel=${f#${SCRIPT_DIR}/}
131
+ add_file "$rel" "${APP_REMOTE_DIR}/${rel}"
132
+ done < <(find "${SCRIPT_DIR}/demo" -type f | sort)
133
+ fi
134
+ fi
135
+
136
+ if [ "$SKIP_NGINX" -ne 1 ]; then
137
+ # Snippets included from the site config.
138
+ if [ -d "${SCRIPT_DIR}/nginx" ]; then
139
+ while IFS= read -r f; do
140
+ rel=${f#${SCRIPT_DIR}/nginx/}
141
+ base=$(basename "$f")
142
+ case "$rel" in
143
+ conf.d/*) add_file "nginx/${rel}" "${NGINX_CONFD_DIR}/${base}" ;;
144
+ wip.computer.conf) ;; # site config: deploy manually, has carry-over
145
+ *) add_file "nginx/${rel}" "${NGINX_SNIPPETS_DIR}/${base}" ;;
146
+ esac
147
+ done < <(find "${SCRIPT_DIR}/nginx" -maxdepth 2 -type f -name '*.conf' | sort)
148
+ fi
149
+ fi
150
+
151
+ # ── Dry-run preview ─────────────────────────────────────────────────
152
+
153
+ if [ "$DRY_RUN" -eq 1 ]; then
154
+ echo "DRY RUN. Git: ${GIT_BRANCH} @ ${GIT_COMMIT} (dirty=${GIT_DIRTY})"
155
+ echo "Remote: ${REMOTE}"
156
+ echo "Files (${#SRC_FILES[@]}):"
157
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
158
+ printf " %-40s -> %s [%s]\n" "${SRC_FILES[$i]}" "${DST_FILES[$i]}" "${SHA_FILES[$i]:0:12}"
159
+ done
160
+ echo ""
161
+ echo "Would write manifest to: ${MANIFEST_DIR}/$(date -u +%Y-%m-%dT%H-%M-%SZ).json"
162
+ exit 0
163
+ fi
164
+
165
+ if [ ${#SRC_FILES[@]} -eq 0 ]; then
166
+ echo "Nothing to deploy (both --skip-app and --skip-nginx, or no files matched)." >&2
167
+ exit 1
168
+ fi
169
+
170
+ # ── Deploy app files ────────────────────────────────────────────────
171
+
172
+ # /var/www/wip.computer/ is root-owned; the manifest dir must be
173
+ # created with sudo on first deploy. After creation, parker:parker
174
+ # ownership lets subsequent deploys write the manifest without sudo.
175
+ ssh "${REMOTE}" "sudo install -d -o parker -g parker -m 0755 ${MANIFEST_DIR}"
176
+
177
+ if [ "$SKIP_APP" -ne 1 ]; then
178
+ ssh "${REMOTE}" "mkdir -p ${APP_REMOTE_DIR}/inbox"
179
+ fi
180
+
181
+ # scp each file. Pre-create destination dir on the remote so app/foo/bar.html
182
+ # does not fail on missing parent.
183
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
184
+ src=${SRC_FILES[$i]}
185
+ dst=${DST_FILES[$i]}
186
+ case "$dst" in
187
+ /etc/nginx/*) need_sudo=1 ;;
188
+ *) need_sudo=0 ;;
189
+ esac
190
+ remote_parent=$(dirname "$dst")
191
+ if [ "$need_sudo" -eq 1 ]; then
192
+ # Stage to a temp dir on remote, then sudo mv into place.
193
+ tmp="/tmp/deploy-staging-$$/$(basename "$src")"
194
+ ssh "${REMOTE}" "mkdir -p $(dirname "$tmp")"
195
+ scp "${SCRIPT_DIR}/${src}" "${REMOTE}:${tmp}"
196
+ ssh "${REMOTE}" "sudo install -m 0644 ${tmp} ${dst}"
197
+ else
198
+ ssh "${REMOTE}" "mkdir -p ${remote_parent}"
199
+ scp "${SCRIPT_DIR}/${src}" "${REMOTE}:${dst}"
200
+ fi
201
+ done
202
+
203
+ # Cleanup staging dir if it exists.
204
+ ssh "${REMOTE}" "rm -rf /tmp/deploy-staging-$$" || true
205
+
206
+ # ── Verify post-deploy hashes (transfer integrity) ──────────────────
207
+
208
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
209
+ remote_sha=$(ssh "${REMOTE}" "sudo sha256sum ${DST_FILES[$i]}" 2>/dev/null | awk '{print $1}' || echo "")
210
+ if [ "${SHA_FILES[$i]}" != "$remote_sha" ]; then
211
+ echo "FATAL: post-deploy hash mismatch for ${SRC_FILES[$i]}" >&2
212
+ echo " local : ${SHA_FILES[$i]}" >&2
213
+ echo " remote: ${remote_sha}" >&2
214
+ exit 1
215
+ fi
216
+ done
217
+
218
+ # ── npm install (app only) ──────────────────────────────────────────
219
+
220
+ if [ "$SKIP_APP" -ne 1 ]; then
221
+ ssh "${REMOTE}" "cd ${APP_REMOTE_DIR} && npm install --omit=dev"
222
+ fi
223
+
224
+ # ── nginx test + reload (nginx only) ────────────────────────────────
225
+
226
+ NGINX_TEST_PASS=null
227
+ NGINX_TEST_OUTPUT_JSON='""'
228
+ if [ "$SKIP_NGINX" -ne 1 ]; then
229
+ set +e
230
+ NGINX_TEST_OUTPUT=$(ssh "${REMOTE}" "sudo nginx -t" 2>&1)
231
+ NGINX_TEST_RC=$?
232
+ set -e
233
+ if [ "$NGINX_TEST_RC" -eq 0 ]; then
234
+ NGINX_TEST_PASS=true
235
+ ssh "${REMOTE}" "sudo systemctl reload nginx"
236
+ else
237
+ NGINX_TEST_PASS=false
238
+ echo "FATAL: nginx -t failed; not reloading." >&2
239
+ echo "$NGINX_TEST_OUTPUT" >&2
240
+ fi
241
+ NGINX_TEST_OUTPUT_JSON=$(printf '%s' "$NGINX_TEST_OUTPUT" | json_escape)
242
+ fi
243
+
244
+ # ── PM2 reload ──────────────────────────────────────────────────────
245
+
246
+ PM2_INFO_JSON=null
247
+ if [ "$SKIP_APP" -ne 1 ]; then
248
+ ssh "${REMOTE}" "cd ${APP_REMOTE_DIR} && (pm2 reload mcp-server || (pm2 start server.mjs --name mcp-server && pm2 save))"
249
+
250
+ # Capture pm2 status for the manifest.
251
+ PM2_RAW=$(ssh "${REMOTE}" "pm2 jlist" 2>/dev/null || echo "[]")
252
+ PM2_INFO_JSON=$(printf '%s' "$PM2_RAW" | python3 -c '
253
+ import json,sys
254
+ data=json.load(sys.stdin)
255
+ target="mcp-server"
256
+ for p in data:
257
+ if p.get("name")==target:
258
+ env=p.get("pm2_env",{})
259
+ print(json.dumps({
260
+ "name": p.get("name"),
261
+ "pid": p.get("pid"),
262
+ "status": env.get("status"),
263
+ "restart_time": env.get("restart_time"),
264
+ "uptime": env.get("pm_uptime"),
265
+ "exec_path": env.get("pm_exec_path"),
266
+ "node_version": env.get("node_version"),
267
+ }))
268
+ break
269
+ else:
270
+ print("null")
271
+ ')
272
+ fi
273
+
274
+ # ── Build manifest ──────────────────────────────────────────────────
275
+
276
+ DEPLOYED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
277
+ TIMESTAMP_SLUG=$(date -u +%Y-%m-%dT%H-%M-%SZ)
278
+ DEPLOYED_BY="$(whoami)@$(hostname)"
279
+
280
+ # files[] JSON
281
+ FILES_JSON=""
282
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
283
+ [ "$i" -eq 0 ] || FILES_JSON+=","
284
+ FILES_JSON+=$(printf '{"source":"%s","destination":"%s","sha256":"%s"}' \
285
+ "${SRC_FILES[$i]}" "${DST_FILES[$i]}" "${SHA_FILES[$i]}")
286
+ done
287
+
288
+ MANIFEST=$(cat <<JSON
289
+ {
290
+ "deployedAt": "${DEPLOYED_AT}",
291
+ "deployedBy": "${DEPLOYED_BY}",
292
+ "git": {
293
+ "remote": "${GIT_REMOTE_URL}",
294
+ "branch": "${GIT_BRANCH}",
295
+ "commit": "${GIT_COMMIT}",
296
+ "dirty": ${GIT_DIRTY}
297
+ },
298
+ "files": [${FILES_JSON}],
299
+ "nginx": {
300
+ "skipped": $([ "$SKIP_NGINX" -eq 1 ] && echo true || echo false),
301
+ "test_pass": ${NGINX_TEST_PASS},
302
+ "test_output": ${NGINX_TEST_OUTPUT_JSON}
303
+ },
304
+ "pm2": ${PM2_INFO_JSON}
57
305
  }
58
- NGINX
59
- "
306
+ JSON
307
+ )
308
+
309
+ # ── Write manifest to VPS ───────────────────────────────────────────
310
+
311
+ MANIFEST_PATH="${MANIFEST_DIR}/${TIMESTAMP_SLUG}.json"
312
+ 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
313
 
61
314
  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'"
315
+ echo "Deploy complete."
316
+ echo "Manifest: ${REMOTE}:${MANIFEST_PATH}"
67
317
  echo ""
68
- echo "Deployment complete."
69
- echo "Health check: curl https://wip.computer/health"
70
- echo "MCP endpoint: https://wip.computer/mcp"
318
+ echo "Verify:"
319
+ echo " curl -fsS https://wip.computer/health"
320
+ echo " bash ${SCRIPT_DIR}/scripts/verify-deploy.sh ${MANIFEST_PATH}"
@@ -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
+ }
@@ -0,0 +1,60 @@
1
+ # nginx log redaction (F-007 in the VPS hosted-mcp audit).
2
+ #
3
+ # Defines a "redacted" log_format that masks bearer tokens, WS tickets,
4
+ # and ck-style API keys in the request line and the Referer header
5
+ # before they are written to access logs. Maps run at http context, so
6
+ # this file deploys to /etc/nginx/conf.d/redact-logs.conf where nginx
7
+ # loads it automatically from inside the http {} block.
8
+ #
9
+ # Source: src/hosted-mcp/nginx/conf.d/redact-logs.conf
10
+ # Deploy: /etc/nginx/conf.d/redact-logs.conf
11
+ #
12
+ # To use this format, server blocks must reference it explicitly on
13
+ # their access_log directive, e.g.
14
+ # access_log /var/log/nginx/wip.computer.access.log redacted;
15
+
16
+ # ── Query-string redaction ──────────────────────────────────────────
17
+ #
18
+ # nginx maps replace the entire value, not a substring, so any value
19
+ # containing one of these patterns gets the whole query string masked.
20
+ # That is correct here: if a request is leaking a token in the query,
21
+ # we do not want to keep the rest of the args either.
22
+
23
+ # Note: regex patterns are double-quoted because nginx's lexer treats
24
+ # bare {N,} quantifiers as config-block delimiters. Quoting forces
25
+ # nginx to treat the whole token as a regex string.
26
+
27
+ map $args $args_redacted {
28
+ "~*(?:^|&)token=" "[REDACTED token=]";
29
+ "~*(?:^|&)ticket=" "[REDACTED ticket=]";
30
+ "~*(?:^|&)api_key=" "[REDACTED api_key=]";
31
+ "~*(?:^|&)access_token=" "[REDACTED access_token=]";
32
+ "~*ck-[A-Za-z0-9_-]{6,}" "[REDACTED ck-]";
33
+ default $args;
34
+ }
35
+
36
+ # ── Referer redaction ───────────────────────────────────────────────
37
+ #
38
+ # Browsers send the previous URL as Referer, which can carry token
39
+ # values across navigations. Mask the same patterns there.
40
+
41
+ map $http_referer $http_referer_redacted {
42
+ "~*[?&]token=" "[REDACTED token in referer]";
43
+ "~*[?&]ticket=" "[REDACTED ticket in referer]";
44
+ "~*[?&]api_key=" "[REDACTED api_key in referer]";
45
+ "~*[?&]access_token=" "[REDACTED access_token in referer]";
46
+ "~*ck-[A-Za-z0-9_-]{6,}" "[REDACTED ck in referer]";
47
+ default $http_referer;
48
+ }
49
+
50
+ # ── Request line built without the raw query ────────────────────────
51
+ #
52
+ # $request expands to the full request line including the raw query.
53
+ # We rebuild it from $request_method + $uri (path only) + the redacted
54
+ # args, so the path is preserved but the query is masked when it
55
+ # contains a token-shaped value.
56
+
57
+ log_format redacted '$remote_addr - $remote_user [$time_local] '
58
+ '"$request_method $uri?$args_redacted $server_protocol" '
59
+ '$status $body_bytes_sent '
60
+ '"$http_referer_redacted" "$http_user_agent"';
@@ -37,6 +37,27 @@ location /webauthn/ {
37
37
  proxy_set_header X-Forwarded-Proto $scheme;
38
38
  }
39
39
 
40
+ # QR login API used by the canonical /login Kaleidoscope desktop-to-phone
41
+ # flow. Without these routes nginx falls through to the static homepage
42
+ # or returns 405, and the browser tries to parse HTML as JSON.
43
+ location = /api/qr-login {
44
+ proxy_pass http://127.0.0.1:18800/api/qr-login;
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
+
52
+ location /api/qr-login/ {
53
+ proxy_pass http://127.0.0.1:18800/api/qr-login/;
54
+ proxy_http_version 1.1;
55
+ proxy_set_header Host $host;
56
+ proxy_set_header X-Real-IP $remote_addr;
57
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
58
+ proxy_set_header X-Forwarded-Proto $scheme;
59
+ }
60
+
40
61
  # Signup and login pages
41
62
  location = /signup {
42
63
  proxy_pass http://127.0.0.1:18800/signup;
@@ -65,6 +86,43 @@ location = /login {
65
86
  proxy_set_header X-Forwarded-Proto $scheme;
66
87
  }
67
88
 
89
+ # Explicit non-primary route for the app/login.html flow. Without
90
+ # this, /login/app falls through to the catch-all `location /` and
91
+ # serves the WIP marketing static. The Node app's /login/app handler
92
+ # is what we actually want here.
93
+ location = /login/app {
94
+ proxy_pass http://127.0.0.1:18800/login/app;
95
+ proxy_http_version 1.1;
96
+ proxy_set_header Host $host;
97
+ proxy_set_header X-Real-IP $remote_addr;
98
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
99
+ proxy_set_header X-Forwarded-Proto $scheme;
100
+ }
101
+ location = /login/app/ {
102
+ proxy_pass http://127.0.0.1:18800/login/app/;
103
+ proxy_http_version 1.1;
104
+ proxy_set_header Host $host;
105
+ proxy_set_header X-Real-IP $remote_addr;
106
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
107
+ proxy_set_header X-Forwarded-Proto $scheme;
108
+ }
109
+
110
+ # Static assets served by Node from src/hosted-mcp/app/.
111
+ # /login (app/kaleidoscope-login.html) references /app/footer.js and
112
+ # /app/sprites.png. Without this proxy, those asset requests fall
113
+ # through to the catch-all `location /` and either 404 or get the
114
+ # static homepage HTML, recreating the "HTML returned where JS/PNG
115
+ # was expected" failure class. server.mjs's serveAppFile sets the
116
+ # correct Content-Type for js, png, html, css, svg, json, ico.
117
+ location /app/ {
118
+ proxy_pass http://127.0.0.1:18800/app/;
119
+ proxy_http_version 1.1;
120
+ proxy_set_header Host $host;
121
+ proxy_set_header X-Real-IP $remote_addr;
122
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
123
+ proxy_set_header X-Forwarded-Proto $scheme;
124
+ }
125
+
68
126
  # Agent approval page (server-generated)
69
127
  location = /approve {
70
128
  proxy_pass http://127.0.0.1:18800/approve;
@@ -4,7 +4,12 @@ server {
4
4
  root /var/www/wip.computer/public_html;
5
5
  index index.html index.htm;
6
6
 
7
- access_log /var/log/nginx/wip.computer.access.log;
7
+ # Log redaction: F-007 in the VPS hosted-mcp audit.
8
+ # The "redacted" format is defined in /etc/nginx/conf.d/redact-logs.conf
9
+ # (source: src/hosted-mcp/nginx/conf.d/redact-logs.conf). It masks
10
+ # token=, ticket=, api_key=, access_token=, and ck- values in the
11
+ # request line and Referer before the line is written.
12
+ access_log /var/log/nginx/wip.computer.access.log redacted;
8
13
  error_log /var/log/nginx/wip.computer.error.log;
9
14
 
10
15
  # Docs redirect to docs.wip.computer
@@ -45,6 +50,25 @@ server {
45
50
  proxy_set_header X-Forwarded-Proto $scheme;
46
51
  }
47
52
 
53
+ # Next.js internal asset surface for the Kaleidoscope app on :3001.
54
+ # /codex-remote-control/<tid> renders a Next HTML shell that
55
+ # references /_next/static/chunks/*.js and /_next/static/chunks/*.css.
56
+ # Without this proxy, those asset requests fall through to
57
+ # `location /` try_files and return the static homepage HTML, which
58
+ # makes the browser fail to hydrate (page renders blank). Same
59
+ # upstream as /codex-remote-control/. Covers /_next/static/* and
60
+ # /_next/image* paths used by the Next runtime.
61
+ location /_next/ {
62
+ proxy_pass http://127.0.0.1:3001;
63
+ proxy_http_version 1.1;
64
+ proxy_set_header Upgrade $http_upgrade;
65
+ proxy_set_header Connection "upgrade";
66
+ proxy_set_header Host $host;
67
+ proxy_set_header X-Real-IP $remote_addr;
68
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
69
+ proxy_set_header X-Forwarded-Proto $scheme;
70
+ }
71
+
48
72
  # Health check
49
73
  location /health {
50
74
  proxy_pass http://127.0.0.1:18800/health;