@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
package/src/hosted-mcp/deploy.sh
CHANGED
|
@@ -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
|
-
# -
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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 "
|
|
63
|
-
echo "
|
|
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 "
|
|
69
|
-
echo "
|
|
70
|
-
echo "
|
|
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
|
-
|
|
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;
|