@wipcomputer/wip-ldm-os 0.4.84 → 0.4.85-alpha.10

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.
@@ -4,106 +4,104 @@ 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
- location / {
11
- autoindex off;
12
- try_files $uri $uri/ /index.html;
15
+ # Docs redirect to docs.wip.computer
16
+ location = /doc {
17
+ return 301 https://docs.wip.computer;
18
+ }
19
+ location = /docs {
20
+ return 301 https://docs.wip.computer;
21
+ }
22
+ location /docs/ {
23
+ return 301 https://docs.wip.computer;
13
24
  }
14
25
 
15
- add_header X-Frame-Options "SAMEORIGIN" always;
16
- add_header X-Content-Type-Options "nosniff" always;
17
- add_header X-XSS-Protection "1; mode=block" always;
26
+ # MCP server
27
+ # OAuth 2.0 for Claude iOS connector
28
+ include snippets/mcp-oauth.conf;
29
+ include snippets/mcp-server.conf;
18
30
 
19
- # ── Codex Remote Control relay ──
20
- # The Node app at 127.0.0.1:18800 (wip-mcp/server.mjs) owns these.
21
- # Without these blocks, nginx falls back to /index.html and the
22
- # phone-side bootstrap + ws-ticket calls receive HTML, which breaks
23
- # E2EE handshake and relay attach. See:
24
- # wip-ldm-os-private/ai/product/plans-prds/codex-remote-control/
25
-
26
- # HTTP routes: bootstrap (GET), ws-ticket (POST), state (GET),
27
- # pair-init (POST), pair-status (GET), pair-complete (POST).
28
- location /api/codex-relay/bootstrap/ {
29
- proxy_pass http://127.0.0.1:18800;
30
- proxy_http_version 1.1;
31
- proxy_set_header Host $host;
32
- proxy_set_header X-Real-IP $remote_addr;
33
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
34
- proxy_set_header X-Forwarded-Proto $scheme;
35
- }
36
- location /api/codex-relay/ws-ticket {
37
- proxy_pass http://127.0.0.1:18800;
31
+ # Codex Remote Control relay (HTTP + WSS proxy_pass to wip-mcp Node app
32
+ # at 127.0.0.1:18800; covers /api/codex-relay/bootstrap, ws-ticket, state,
33
+ # pair-init/status/complete, web/<tid>, daemon).
34
+ include snippets/codex-relay.conf;
35
+
36
+ # Codex Remote Control phone surface
37
+ # The Next.js app at kaleidoscope.wip.computer (port 3001) renders
38
+ # /codex-remote-control/[threadId]. The MCP tool's auth URL uses
39
+ # wip.computer as the origin, so wip.computer/codex-remote-control/<tid>
40
+ # must reach the same Next.js app. WebSocket Upgrade headers included
41
+ # so live-reload / RSC streams work cleanly.
42
+ location /codex-remote-control/ {
43
+ proxy_pass http://127.0.0.1:3001;
38
44
  proxy_http_version 1.1;
45
+ proxy_set_header Upgrade $http_upgrade;
46
+ proxy_set_header Connection "upgrade";
39
47
  proxy_set_header Host $host;
40
48
  proxy_set_header X-Real-IP $remote_addr;
41
49
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
42
50
  proxy_set_header X-Forwarded-Proto $scheme;
43
51
  }
44
- location /api/codex-relay/state {
45
- proxy_pass http://127.0.0.1:18800;
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;
46
63
  proxy_http_version 1.1;
64
+ proxy_set_header Upgrade $http_upgrade;
65
+ proxy_set_header Connection "upgrade";
47
66
  proxy_set_header Host $host;
48
67
  proxy_set_header X-Real-IP $remote_addr;
49
68
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
50
69
  proxy_set_header X-Forwarded-Proto $scheme;
51
70
  }
52
- location /api/codex-relay/pair-init {
53
- proxy_pass http://127.0.0.1:18800;
71
+
72
+ # Health check
73
+ location /health {
74
+ proxy_pass http://127.0.0.1:18800/health;
54
75
  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
76
  }
60
- location /api/codex-relay/pair-status/ {
61
- proxy_pass http://127.0.0.1:18800;
62
- proxy_http_version 1.1;
63
- proxy_set_header Host $host;
64
- proxy_set_header X-Real-IP $remote_addr;
65
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
66
- proxy_set_header X-Forwarded-Proto $scheme;
67
- }
68
- location /api/codex-relay/pair-complete {
69
- proxy_pass http://127.0.0.1:18800;
70
- proxy_http_version 1.1;
71
- proxy_set_header Host $host;
72
- proxy_set_header X-Real-IP $remote_addr;
73
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
74
- proxy_set_header X-Forwarded-Proto $scheme;
77
+
78
+
79
+ # Demo pages (static files)
80
+ location /demo {
81
+ alias /var/www/wip.computer/app/mcp-server/demo;
82
+ index index.html;
83
+ try_files $uri $uri/ $uri/index.html =404;
75
84
  }
76
85
 
77
- # WebSocket routes: phone (web) and daemon long-lived sockets.
78
- # Upgrade headers are the standard WebSocket-via-nginx pattern.
79
- # Long read timeout because daemon sockets stay open indefinitely.
80
- location /api/codex-relay/web/ {
81
- proxy_pass http://127.0.0.1:18800;
86
+ # Demo API (proxied to MCP server)
87
+ location /demo/api/ {
88
+ proxy_pass http://127.0.0.1:18800/demo/api/;
82
89
  proxy_http_version 1.1;
83
- proxy_set_header Upgrade $http_upgrade;
84
- proxy_set_header Connection "upgrade";
85
90
  proxy_set_header Host $host;
86
91
  proxy_set_header X-Real-IP $remote_addr;
87
92
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
88
93
  proxy_set_header X-Forwarded-Proto $scheme;
89
- proxy_read_timeout 86400;
90
- proxy_send_timeout 86400;
91
- proxy_buffering off;
92
94
  }
93
- location /api/codex-relay/daemon {
94
- proxy_pass http://127.0.0.1:18800;
95
- proxy_http_version 1.1;
96
- proxy_set_header Upgrade $http_upgrade;
97
- proxy_set_header Connection "upgrade";
98
- proxy_set_header Host $host;
99
- proxy_set_header X-Real-IP $remote_addr;
100
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
101
- proxy_set_header X-Forwarded-Proto $scheme;
102
- proxy_read_timeout 86400;
103
- proxy_send_timeout 86400;
104
- proxy_buffering off;
95
+
96
+ location / {
97
+ autoindex off;
98
+ try_files $uri $uri/ /index.html;
105
99
  }
106
100
 
101
+ add_header X-Frame-Options "SAMEORIGIN" always;
102
+ add_header X-Content-Type-Options "nosniff" always;
103
+ add_header X-XSS-Protection "1; mode=block" always;
104
+
107
105
  location ~ /\. {
108
106
  deny all;
109
107
  }
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ # audit-logs.sh: F-007 pre-fix log audit for the VPS hosted-mcp surface.
3
+ #
4
+ # Read-only. No log mutation, no rotation, no clearing. Greps nginx and
5
+ # PM2 logs for bearer/ticket/api-key shaped values that may have leaked
6
+ # during the period when the WS upgrade accepted ?token=ck- and the
7
+ # phone app sent ck values in URLs.
8
+ #
9
+ # Output: per-source match count, plus up to N redacted sample lines per
10
+ # source so you can see which path / agent / time range is affected
11
+ # without ever printing the leaked secret in this terminal session.
12
+ #
13
+ # If any source shows a non-zero count, treat the matching ck values as
14
+ # already-leaked. Rotate them as part of the F-002 + F-005a deploy
15
+ # (see ai/product/bugs/security/2026-04-28--cc-mini--vps-hosted-mcp-audit.md).
16
+ #
17
+ # Usage on the VPS:
18
+ # bash audit-logs.sh
19
+ # bash audit-logs.sh --days 14 # widen log window for rotated logs
20
+ # bash audit-logs.sh --samples 20 # show more redacted samples
21
+ # bash audit-logs.sh --json # machine-readable output (counts only)
22
+ #
23
+ # Source: src/hosted-mcp/scripts/audit-logs.sh
24
+
25
+ set -euo pipefail
26
+
27
+ # ── Defaults ────────────────────────────────────────────────────────
28
+
29
+ DAYS=7
30
+ SAMPLES=10
31
+ JSON=0
32
+ NGINX_LOG_DIR=/var/log/nginx
33
+ PM2_APP=mcp-server
34
+
35
+ # ── Args ────────────────────────────────────────────────────────────
36
+
37
+ while [ $# -gt 0 ]; do
38
+ case "$1" in
39
+ --days) DAYS=$2; shift 2 ;;
40
+ --samples) SAMPLES=$2; shift 2 ;;
41
+ --json) JSON=1; shift 1 ;;
42
+ --nginx-log-dir) NGINX_LOG_DIR=$2; shift 2 ;;
43
+ --pm2-app) PM2_APP=$2; shift 2 ;;
44
+ -h|--help)
45
+ sed -n '2,30p' "$0"
46
+ exit 0
47
+ ;;
48
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
49
+ esac
50
+ done
51
+
52
+ # ── Patterns ────────────────────────────────────────────────────────
53
+ #
54
+ # Three families of leak we are looking for. Each is a single ERE that
55
+ # `grep -E` will accept. We match conservatively (\b, exact prefixes)
56
+ # so we do not over-flag random bytes that happen to resemble a key.
57
+
58
+ PAT_QUERY_TOKEN='[?&](token|ticket|api_key|access_token)=[^[:space:]&"]+'
59
+ PAT_CK_KEY='\bck-[A-Za-z0-9_-]{6,}\b'
60
+ PAT_AUTH_BEARER='Authorization:[[:space:]]*Bearer[[:space:]]+ck-[A-Za-z0-9_-]{6,}'
61
+
62
+ # ── Redaction (for sample output only) ──────────────────────────────
63
+ #
64
+ # Replace the matched value with a marker before we print the sample
65
+ # line. This way the audit's own output never reproduces the leaked
66
+ # secret in the operator's terminal/scrollback.
67
+
68
+ redact() {
69
+ sed -E \
70
+ -e 's/([?&](token|ticket|api_key|access_token)=)[^&" \t]+/\1[REDACTED]/g' \
71
+ -e 's/\bck-[A-Za-z0-9_-]+/[REDACTED ck-]/g'
72
+ }
73
+
74
+ # ── Source inventory ────────────────────────────────────────────────
75
+
76
+ declare -a SOURCES
77
+ declare -A SOURCE_LABEL
78
+
79
+ add_source() {
80
+ local label=$1
81
+ local path=$2
82
+ if [ -e "$path" ] || compgen -G "$path" > /dev/null; then
83
+ SOURCES+=("$path")
84
+ SOURCE_LABEL["$path"]=$label
85
+ fi
86
+ }
87
+
88
+ add_source "nginx access (current)" "${NGINX_LOG_DIR}/access.log"
89
+ add_source "nginx error (current)" "${NGINX_LOG_DIR}/error.log"
90
+ add_source "nginx wip.computer.access" "${NGINX_LOG_DIR}/wip.computer.access.log"
91
+ add_source "nginx wip.computer.error" "${NGINX_LOG_DIR}/wip.computer.error.log"
92
+
93
+ # ── Counters ────────────────────────────────────────────────────────
94
+
95
+ total_matches=0
96
+ declare -A counts
97
+
98
+ scan_one() {
99
+ local path=$1
100
+ local label=$2
101
+ local count
102
+ if [[ "$path" == *.gz ]]; then
103
+ count=$(sudo zgrep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null | wc -l | tr -d ' ')
104
+ else
105
+ count=$(sudo grep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null | wc -l | tr -d ' ')
106
+ fi
107
+ counts["$path"]=$count
108
+ total_matches=$(( total_matches + count ))
109
+ }
110
+
111
+ print_samples() {
112
+ local path=$1
113
+ local label=$2
114
+ local count=${counts["$path"]}
115
+ [ "$count" -eq 0 ] && return
116
+ echo
117
+ echo "── $label ($path)"
118
+ echo " matches: $count"
119
+ echo " sample (redacted, up to $SAMPLES lines):"
120
+ if [[ "$path" == *.gz ]]; then
121
+ sudo zgrep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null \
122
+ | head -n "$SAMPLES" | redact | sed 's/^/ /'
123
+ else
124
+ sudo grep -E "$PAT_QUERY_TOKEN|$PAT_CK_KEY|$PAT_AUTH_BEARER" "$path" 2>/dev/null \
125
+ | head -n "$SAMPLES" | redact | sed 's/^/ /'
126
+ fi
127
+ }
128
+
129
+ # ── Run nginx scan ──────────────────────────────────────────────────
130
+
131
+ if [ "$JSON" -ne 1 ]; then
132
+ echo "F-007 log audit"
133
+ echo "Days back (rotated): $DAYS"
134
+ echo "Samples per source: $SAMPLES"
135
+ echo "Nginx log dir: $NGINX_LOG_DIR"
136
+ echo
137
+ fi
138
+
139
+ # Current logs
140
+ for path in "${SOURCES[@]}"; do
141
+ scan_one "$path" "${SOURCE_LABEL[$path]}"
142
+ done
143
+
144
+ # Rotated logs within the window
145
+ shopt -s nullglob
146
+ declare -a ROTATED
147
+ for f in "${NGINX_LOG_DIR}"/*.gz "${NGINX_LOG_DIR}"/*.[0-9]*; do
148
+ if [ -f "$f" ]; then
149
+ if find "$f" -mtime "-${DAYS}" -print -quit | grep -q .; then
150
+ ROTATED+=("$f")
151
+ fi
152
+ fi
153
+ done
154
+
155
+ for path in "${ROTATED[@]}"; do
156
+ scan_one "$path" "rotated $(basename "$path")"
157
+ done
158
+
159
+ # ── PM2 ─────────────────────────────────────────────────────────────
160
+ #
161
+ # pm2 logs --nostream prints recent lines from the in-memory buffer.
162
+ # This is best-effort: it covers the current PM2 process lifetime, not
163
+ # rotated copies. PM2's persisted logs (if any) live under
164
+ # ~/.pm2/logs/<app>-out.log and ~/.pm2/logs/<app>-error.log.
165
+
166
+ PM2_OUT="$HOME/.pm2/logs/${PM2_APP}-out.log"
167
+ PM2_ERR="$HOME/.pm2/logs/${PM2_APP}-error.log"
168
+
169
+ if [ -e "$PM2_OUT" ]; then add_source "pm2 ${PM2_APP} out" "$PM2_OUT"; scan_one "$PM2_OUT" "pm2 out"; fi
170
+ if [ -e "$PM2_ERR" ]; then add_source "pm2 ${PM2_APP} error" "$PM2_ERR"; scan_one "$PM2_ERR" "pm2 error"; fi
171
+
172
+ # ── Output ──────────────────────────────────────────────────────────
173
+
174
+ if [ "$JSON" -eq 1 ]; then
175
+ printf '{"total_matches": %d, "by_source": {' "$total_matches"
176
+ first=1
177
+ for path in "${!counts[@]}"; do
178
+ [ "$first" -eq 1 ] && first=0 || printf ','
179
+ printf '"%s": %d' "$path" "${counts[$path]}"
180
+ done
181
+ printf '}}\n'
182
+ else
183
+ echo "── Summary"
184
+ for path in "${!counts[@]}"; do
185
+ label=${SOURCE_LABEL[$path]:-${path}}
186
+ printf " %-40s %s matches\n" "$label" "${counts[$path]}"
187
+ done
188
+ echo
189
+ echo "── Total matches: $total_matches"
190
+
191
+ for path in "${!counts[@]}"; do
192
+ print_samples "$path" "${SOURCE_LABEL[$path]:-${path}}"
193
+ done
194
+
195
+ echo
196
+ if [ "$total_matches" -gt 0 ]; then
197
+ echo "ACTION REQUIRED:"
198
+ echo " Treat any matched ck values as leaked. Rotate them as part of the"
199
+ echo " F-002 + F-005a deploy. After deploying redact-logs.conf, re-run"
200
+ echo " this script and confirm zero matches in the post-rotation window."
201
+ else
202
+ echo "OK: no leak signatures found in the scanned window."
203
+ echo " Re-run after every deploy that adds or moves auth surfaces."
204
+ fi
205
+ fi
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # verify-deploy.sh: Verify a hosted-mcp deploy manifest against the live
3
+ # VPS. Reads a manifest produced by deploy.sh, fetches the current
4
+ # sha256 of each declared destination on the remote, and compares.
5
+ #
6
+ # Per F-009 of the VPS hosted-mcp audit. Use after deploy.sh, after
7
+ # any apparent change to the live tree, or as part of the pre-dogfood
8
+ # smoke test.
9
+ #
10
+ # Usage:
11
+ # bash verify-deploy.sh wip.computer:/var/www/.../<timestamp>.json
12
+ # bash verify-deploy.sh /path/to/local/manifest.json --remote wip.computer
13
+ # bash verify-deploy.sh latest # use latest manifest on VPS
14
+ #
15
+ # Exits non-zero on any mismatch. Output lists each file as OK or DIFF.
16
+
17
+ set -euo pipefail
18
+
19
+ REMOTE="wip.computer"
20
+ MANIFEST_DIR="/var/www/wip.computer/deploy-manifests/hosted-mcp"
21
+ ARG=""
22
+
23
+ while [ $# -gt 0 ]; do
24
+ case "$1" in
25
+ --remote) REMOTE=$2; shift 2 ;;
26
+ -h|--help) sed -n '2,18p' "$0"; exit 0 ;;
27
+ *) ARG=$1; shift ;;
28
+ esac
29
+ done
30
+
31
+ if [ -z "$ARG" ]; then
32
+ echo "usage: bash verify-deploy.sh <manifest-path-or-latest>" >&2
33
+ exit 2
34
+ fi
35
+
36
+ # Resolve manifest path.
37
+ if [ "$ARG" = "latest" ]; then
38
+ MANIFEST_PATH=$(ssh "${REMOTE}" "ls -t ${MANIFEST_DIR}/*.json 2>/dev/null | head -1")
39
+ if [ -z "$MANIFEST_PATH" ]; then
40
+ echo "FATAL: no manifests found in ${REMOTE}:${MANIFEST_DIR}" >&2
41
+ exit 1
42
+ fi
43
+ echo "Latest manifest: ${MANIFEST_PATH}"
44
+ MANIFEST_JSON=$(ssh "${REMOTE}" "cat ${MANIFEST_PATH}")
45
+ elif [[ "$ARG" == *":"* ]]; then
46
+ # remote:path form
47
+ MANIFEST_REMOTE=${ARG%%:*}
48
+ MANIFEST_PATH=${ARG#*:}
49
+ MANIFEST_JSON=$(ssh "${MANIFEST_REMOTE}" "cat ${MANIFEST_PATH}")
50
+ elif [ -f "$ARG" ]; then
51
+ MANIFEST_PATH=$ARG
52
+ MANIFEST_JSON=$(cat "$ARG")
53
+ else
54
+ echo "FATAL: manifest not found: ${ARG}" >&2
55
+ exit 1
56
+ fi
57
+
58
+ # Parse files[] entries via python3.
59
+ if ! command -v python3 >/dev/null 2>&1; then
60
+ echo "FATAL: python3 required" >&2
61
+ exit 1
62
+ fi
63
+
64
+ ENTRIES=$(printf '%s' "$MANIFEST_JSON" | python3 -c '
65
+ import json,sys
66
+ m=json.load(sys.stdin)
67
+ for f in m.get("files",[]):
68
+ print(f.get("source","")+"\t"+f.get("destination","")+"\t"+f.get("sha256",""))
69
+ ')
70
+
71
+ if [ -z "$ENTRIES" ]; then
72
+ echo "FATAL: no files entries in manifest" >&2
73
+ exit 1
74
+ fi
75
+
76
+ echo "Manifest: ${MANIFEST_PATH}"
77
+ echo "Verifying ${REMOTE}:"
78
+ echo
79
+
80
+ OK=0
81
+ FAIL=0
82
+ while IFS=$'\t' read -r src dst expected; do
83
+ [ -z "$dst" ] && continue
84
+ # ssh -n redirects stdin from /dev/null so ssh does not consume the
85
+ # loop's heredoc and short-circuit after the first iteration.
86
+ remote_sha=$(ssh -n "${REMOTE}" "sudo sha256sum ${dst} 2>/dev/null" | awk '{print $1}' || echo "")
87
+ if [ -z "$remote_sha" ]; then
88
+ printf " MISSING %s\n" "$dst"
89
+ FAIL=$((FAIL+1))
90
+ elif [ "$remote_sha" = "$expected" ]; then
91
+ printf " OK %s\n" "$dst"
92
+ OK=$((OK+1))
93
+ else
94
+ printf " DIFF %s\n expected %s\n live %s\n" "$dst" "$expected" "$remote_sha"
95
+ FAIL=$((FAIL+1))
96
+ fi
97
+ done <<< "$ENTRIES"
98
+
99
+ echo
100
+ echo "Summary: ${OK} ok, ${FAIL} mismatched"
101
+ [ "$FAIL" -eq 0 ] && exit 0
102
+ exit 1