@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.
- package/bin/ldm.js +18 -0
- 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
|
@@ -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
|