Skip to content

Commit c0af4e7

Browse files
authored
feat(mcp): base-to-base sync, idle park Chromium, multiSelect updates (#19)
feat(mcp): base-to-base sync, idle park Chromium, multiSelect updates
2 parents 243c620 + a5d5f43 commit c0af4e7

190 files changed

Lines changed: 49773 additions & 853 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 142 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@ jobs:
5858
# scope, so a compromised latest release must not be auto-pulled here.
5959
- run: pnpm install -wD @vscode/vsce@3.9.2 ovsx@1.0.1
6060

61+
# Re-derive every native-binary digest from the registry and diff it
62+
# against the committed pin file, BEFORE anything is packaged.
63+
#
64+
# What this uniquely catches: a pin file that is internally consistent —
65+
# correct version, correct integrity — but carries WRONG DIGESTS. The two
66+
# neighbouring failure modes are already covered offline and would never
67+
# reach this step: a stale or missing pin fails closed in `binaryPin()`
68+
# during vendoring, and a re-published tarball fails the lockfile SHA-512
69+
# on download. A poisoned pin file is the residual trust root, and it is
70+
# the one thing only a fresh re-derivation can expose.
71+
#
72+
# Placed here deliberately: the first publish is far below and the version
73+
# bump/commit/tag lower still, so a failure here ships nothing and tags
74+
# nothing. Kept OUT of `pnpm test` — it needs the network and pulls ~150 MB.
75+
#
76+
# THIS STEP IS THE GATE. `.github/workflows/verify-binary-pins.yml` runs
77+
# the same check on pull requests, but it is path-filtered and therefore
78+
# SKIPPED on most PRs — and a skipped run is not a verification. That
79+
# workflow is early warning for the contributor; this step is the safety
80+
# boundary. Do not remove it on the grounds that the PR job covers it.
81+
- name: Verify native-binary digest pins
82+
if: inputs.target == 'extension' || inputs.target == 'both'
83+
run: pnpm check:binary-digests
84+
6185
# ── Compute versions ───────────────────────────────────────
6286
# For each target, we check both package.json AND the registry.
6387
# We bump from whichever is higher, so we never collide with
@@ -154,13 +178,27 @@ jobs:
154178
echo "current=${BASE}" >> $GITHUB_OUTPUT
155179
echo "next=${NEXT}" >> $GITHUB_OUTPUT
156180
157-
# Write new version to package.json
181+
# Write new version to package.json AND to server.json.
182+
#
183+
# server.json is the MCP Registry manifest and carries its own version in
184+
# two places. check-tool-sync asserts server.json.version === package.json
185+
# .version, and `pnpm build` runs that guard first — so bumping only
186+
# package.json here fails the build on EVERY mcp-server release. It cannot
187+
# be worked around by hand-bumping server.json before dispatch either: the
188+
# BASE above is max(local, published), so the workflow always lands one
189+
# above whatever is committed. The bump has to happen here, together.
158190
node -e "
159191
const fs = require('fs');
160192
const p = '$PKG';
161193
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
162194
pkg.version = '${NEXT}';
163195
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
196+
197+
const sp = 'packages/mcp-server/server.json';
198+
const srv = JSON.parse(fs.readFileSync(sp, 'utf8'));
199+
srv.version = '${NEXT}';
200+
for (const entry of srv.packages ?? []) entry.version = '${NEXT}';
201+
fs.writeFileSync(sp, JSON.stringify(srv, null, 2) + '\n');
164202
"
165203
echo "MCP server: ${BASE} → ${NEXT}"
166204
@@ -218,14 +256,25 @@ jobs:
218256
- name: Run tests
219257
run: pnpm test
220258

221-
# ── Package extension VSIX ─────────────────────────────────
222-
- name: Package VSIX
259+
# ── Package extension VSIX (one per platform target) ───────
260+
#
261+
# `impit` and `@ngrok/ngrok` keep their compiled .node binary in separate
262+
# per-platform npm packages, so a single untargeted VSIX built on this
263+
# ubuntu runner would ship Linux-x64 binaries to every user and break
264+
# `mcp.httpClient: "impit"` and the ngrok tunnel everywhere else. We build
265+
# one VSIX per target instead, each vendoring only its own platform
266+
# packages (pinned to pnpm-lock.yaml by version AND integrity), and assert
267+
# the contents of every artifact before anything can be published.
268+
# The target matrix lives in scripts/vsix-targets.mjs.
269+
#
270+
# This runs on ONE runner rather than a job matrix: the foreign binaries
271+
# are fetched as plain tarballs (nothing is compiled), which avoids
272+
# needing arm64/alpine runners and keeps the version-bump → publish →
273+
# commit → tag sequence below in a single, ordered job.
274+
- name: Package platform-specific VSIXes
223275
id: vsix
224276
if: inputs.target == 'extension' || inputs.target == 'both'
225277
run: |
226-
# Prepare deps for VSIX
227-
node scripts/prepare-package-deps.mjs
228-
229278
# Copy README: strip SVGs + collapse <picture> to single <img> for VS Code Marketplace
230279
node -e "
231280
const fs = require('fs');
@@ -245,19 +294,45 @@ jobs:
245294
fs.writeFileSync('packages/extension/README.md', readme);
246295
"
247296
248-
# Package
249-
cd packages/extension
250-
pnpm exec vsce package --no-dependencies
251-
VSIX=$(ls *.vsix | head -1)
252-
echo "file=packages/extension/${VSIX}" >> $GITHUB_OUTPUT
253-
echo "Packaged: ${VSIX}"
297+
# Eight target artifact packaging/assertion smokes: build one VSIX per
298+
# published target and verify the CONTENTS of each — every .node
299+
# matched byte-for-byte against scripts/native-binary-digests.json,
300+
# which was recorded from tarballs verified against pnpm-lock.yaml.
301+
# package-targets.mjs aborts on the first target that fails, so a bad
302+
# artifact can never reach a publish step.
303+
#
304+
# These are packaging smokes, not runtime smokes. A single runner can
305+
# only ever `require()` the binding built for itself, so only the host
306+
# target's binding could receive a genuine runtime load; the other
307+
# seven are verified by exact content, which is what the digests are
308+
# for. Nothing here should be described as runtime-loading eight
309+
# native bindings.
310+
node scripts/package-targets.mjs --out-dir=artifacts
311+
312+
# Re-assert the finished set: every published target present, no
313+
# untargeted artifact, right binaries — byte-exact — in each.
314+
node scripts/assert-vsix-binaries.mjs --dir=artifacts
315+
316+
ls -la artifacts/
254317
255318
# ── Publish extension ──────────────────────────────────────
319+
# Every artifact is published; there is deliberately NO untargeted
320+
# fallback. A user on a platform we do not build for sees the extension
321+
# as unavailable rather than installing one with the wrong binaries.
256322
- name: Publish extension to VS Code Marketplace
257323
if: |
258324
!inputs.dry_run &&
259325
(inputs.target == 'extension' || inputs.target == 'both')
260-
run: pnpm exec vsce publish --packagePath "${{ steps.vsix.outputs.file }}"
326+
run: |
327+
ARGS=()
328+
COUNT=0
329+
for f in artifacts/*.vsix; do
330+
# Two argv elements per file, so ${#ARGS[@]} is not the build count.
331+
ARGS+=(--packagePath "$f")
332+
COUNT=$((COUNT + 1))
333+
done
334+
echo "Publishing ${COUNT} platform builds to the Marketplace..."
335+
pnpm exec vsce publish "${ARGS[@]}"
261336
env:
262337
VSCE_PAT: ${{ secrets.VSCE_PAT }}
263338

@@ -266,26 +341,51 @@ jobs:
266341
!inputs.dry_run &&
267342
(inputs.target == 'extension' || inputs.target == 'both')
268343
run: |
269-
pnpm exec ovsx publish "${{ steps.vsix.outputs.file }}" --pat "$OVSX_PAT"
344+
for f in artifacts/*.vsix; do
345+
echo "Publishing $(basename "$f") to Open VSX..."
346+
pnpm exec ovsx publish "$f" --pat "$OVSX_PAT"
347+
done
270348
271-
# Verify the version actually landed (Open VSX indexes asynchronously)
349+
# Verify each target actually landed (Open VSX indexes asynchronously).
350+
# Target list comes from the same source of truth the build used.
272351
VERSION="${{ steps.ext_version.outputs.next }}"
273-
echo "Verifying Open VSX indexed v${VERSION}..."
274-
for i in $(seq 1 18); do
275-
FOUND=$(curl -s "https://open-vsx.org/api/Nskha/airtable-formula/${VERSION}" \
276-
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
277-
try{const p=JSON.parse(d);console.log(p.version||'not-found')}
278-
catch{console.log('error')}
279-
})")
280-
if [[ "$FOUND" == "$VERSION" ]]; then
281-
echo "✓ Open VSX confirmed: v${VERSION}"
282-
break
283-
fi
284-
echo " not indexed yet (attempt $i/18), waiting 10s..."
285-
sleep 10
352+
TARGETS=$(node -e "import('./scripts/vsix-targets.mjs').then(m=>console.log(m.ALL_TARGETS.join(' ')))")
353+
354+
# ONE wall-clock budget shared by all targets, enforced whether or not
355+
# anything has been confirmed yet. A per-target budget would multiply
356+
# by the number of targets, so an index that never catches up (queued
357+
# publish, namespace problem) would sleep ~24 minutes and emit a
358+
# warning per target. This is a best-effort check that never fails the
359+
# release, so it must not dominate the job's runtime.
360+
VERIFY_BUDGET=180
361+
VERIFY_START=$(date +%s)
362+
UNVERIFIED=()
363+
364+
for TARGET in $TARGETS; do
365+
echo "Verifying Open VSX indexed v${VERSION} (${TARGET})..."
366+
FOUND=""
367+
while true; do
368+
FOUND=$(curl -s "https://open-vsx.org/api/Nskha/airtable-formula/${TARGET}/${VERSION}" \
369+
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
370+
try{const p=JSON.parse(d);console.log(p.version||'not-found')}
371+
catch{console.log('error')}
372+
})")
373+
if [ "$FOUND" = "$VERSION" ]; then
374+
echo "✓ Open VSX confirmed: v${VERSION} (${TARGET})"
375+
break
376+
fi
377+
if [ $(( $(date +%s) - VERIFY_START )) -ge "$VERIFY_BUDGET" ]; then
378+
UNVERIFIED+=("$TARGET")
379+
break
380+
fi
381+
echo " not indexed yet, waiting 10s..."
382+
sleep 10
383+
done
286384
done
287-
if [[ "$FOUND" != "$VERSION" ]]; then
288-
echo "::warning::Open VSX did not index v${VERSION} within 3 minutes — indexing may still be in progress (publish succeeded)"
385+
386+
ELAPSED=$(( $(date +%s) - VERIFY_START ))
387+
if [ ${#UNVERIFIED[@]} -gt 0 ]; then
388+
echo "::warning::Open VSX did not index v${VERSION} for ${UNVERIFIED[*]} within ${ELAPSED}s — indexing may still be in progress (publish succeeded)"
289389
fi
290390
env:
291391
OVSX_PAT: ${{ secrets.OVSX_PAT }}
@@ -330,7 +430,9 @@ jobs:
330430
fi
331431
332432
if [[ "${{ inputs.target }}" == "mcp-server" || "${{ inputs.target }}" == "both" ]]; then
333-
git add packages/mcp-server/package.json
433+
# server.json is bumped alongside package.json (see the bump step) and
434+
# must be committed with it, or the next build fails the version guard.
435+
git add packages/mcp-server/package.json packages/mcp-server/server.json
334436
TARGETS="${TARGETS} mcp-server v${{ steps.mcp_version.outputs.next }}"
335437
TAGS="${TAGS} mcp-server/v${{ steps.mcp_version.outputs.next }}"
336438
fi
@@ -358,8 +460,9 @@ jobs:
358460
!inputs.dry_run &&
359461
(inputs.target == 'extension' || inputs.target == 'both')
360462
run: |
463+
# Attach every platform build so users can sideload the right one.
361464
gh release create "extension/v${{ steps.ext_version.outputs.next }}" \
362-
"${{ steps.vsix.outputs.file }}" \
465+
artifacts/*.vsix \
363466
--title "Extension v${{ steps.ext_version.outputs.next }}" \
364467
--generate-notes \
365468
--target main
@@ -402,6 +505,13 @@ jobs:
402505
fi
403506
if [[ "${{ inputs.target }}" == "extension" || "${{ inputs.target }}" == "both" ]]; then
404507
echo "- Extension: ${{ steps.ext_version.outputs.current }} → **${{ steps.ext_version.outputs.next }}**" >> $GITHUB_STEP_SUMMARY
508+
if [[ -d artifacts ]]; then
509+
echo "" >> $GITHUB_STEP_SUMMARY
510+
echo " Platform builds (no untargeted fallback is published):" >> $GITHUB_STEP_SUMMARY
511+
for f in artifacts/*.vsix; do
512+
echo " - \`$(basename "$f")\`" >> $GITHUB_STEP_SUMMARY
513+
done
514+
fi
405515
fi
406516
if [[ "${{ inputs.target }}" == "mcp-server" || "${{ inputs.target }}" == "both" ]]; then
407517
echo "- MCP Server: ${{ steps.mcp_version.outputs.current }} → **${{ steps.mcp_version.outputs.next }}**" >> $GITHUB_STEP_SUMMARY
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: Verify native-binary pins
2+
3+
# Re-derive every platform package's `.node` digests from the registry and diff
4+
# them against the committed pin file.
5+
#
6+
# ─────────────────────────────────────────────────────────────────────────────
7+
# THIS JOB IS ADVISORY. IT IS NOT THE GATE. READ THIS BEFORE CHANGING EITHER.
8+
#
9+
# It is path-filtered, so on most pull requests it does not run at all — and a
10+
# job that does not run reports as SKIPPED, which a merge queue or a required-
11+
# check configuration can render as green. **A skipped run is not a
12+
# verification.** Nothing about a green PR here proves the pins were checked.
13+
#
14+
# The authoritative check is the `Verify native-binary digest pins` step in
15+
# .github/workflows/release.yml, which sits between `pnpm install
16+
# --frozen-lockfile` and packaging. That one runs unconditionally on every
17+
# extension release, and nothing publishes or tags if it fails. THAT is the
18+
# safety boundary.
19+
#
20+
# Two consequences, both easy to get wrong later:
21+
# - Do NOT promote this workflow to a required status check believing it
22+
# gates merges. Path filtering means it will pass by absence.
23+
# - Do NOT delete the release-workflow step believing this job covers it.
24+
# It does not, and cannot.
25+
#
26+
# This was a deliberate trade. The alternatives — a companion always-succeeds
27+
# job to make the skip explicit, or dropping the `paths` filter so it runs on
28+
# every PR — each cost more (a permanently-green decoy check, or ~150 MB of
29+
# tarball downloads on every unrelated PR) than they buy, precisely BECAUSE the
30+
# release step is already the real gate. What this job buys is early warning for
31+
# the contributor, in the PR that caused the problem.
32+
# ─────────────────────────────────────────────────────────────────────────────
33+
#
34+
# WHY A SEPARATE, PATH-FILTERED WORKFLOW
35+
#
36+
# This is the only check in the repo that needs network access and pulls ~150 MB
37+
# of tarballs, so it is deliberately kept out of `pnpm test` and out of ci.yml's
38+
# per-OS matrix. It only has anything to say when the pins or the lockfile move,
39+
# which is exactly what `paths` expresses — and GitHub applies `paths` at the
40+
# trigger level, hence its own file rather than a job in ci.yml.
41+
#
42+
# WHY IT RUNS ON THE PR AS WELL AS AT RELEASE
43+
#
44+
# `pnpm-lock.yaml` changing is how an impit / @ngrok/ngrok bump arrives. With
45+
# only the release-time check, a stale pin surfaces to whoever runs the release
46+
# — long after the person who bumped the dependency has moved on. Here it
47+
# surfaces to the author, in the PR that caused it, with the command to fix it.
48+
# That is a convenience for the contributor, not a second safety boundary.
49+
#
50+
# WHAT IT UNIQUELY CATCHES
51+
#
52+
# A pin file that is internally consistent — correct version, correct integrity
53+
# — but carries WRONG DIGESTS. The neighbouring failure modes are already
54+
# covered offline and never reach here: a stale or missing pin fails closed in
55+
# `binaryPin()` during vendoring, and a re-published tarball fails the lockfile
56+
# SHA-512 on download. A poisoned pin file is the residual trust root, and only
57+
# a fresh re-derivation from the registry can expose it.
58+
59+
on:
60+
pull_request:
61+
paths:
62+
# Any of these can invalidate the pin file — two of them WITHOUT editing it:
63+
- 'pnpm-lock.yaml' # an impit / @ngrok/ngrok bump
64+
- 'scripts/native-binary-digests.json' # the pins themselves
65+
- 'scripts/record-native-binary-digests.mjs' # HOW digests are produced
66+
- 'scripts/vsix-targets.mjs' # WHICH packages are expected at all
67+
- 'scripts/safe-symlinks.mjs' # the symlink-escape guard the check relies on
68+
- 'scripts/vendor-platform-packages.mjs' # tarball fetch/verify/vendor logic the check imports
69+
- 'package.json' # the `check:binary-digests` script definition itself
70+
71+
permissions:
72+
contents: read
73+
74+
jobs:
75+
verify-pins:
76+
runs-on: ubuntu-latest
77+
steps:
78+
- uses: actions/checkout@v4
79+
- uses: pnpm/action-setup@v4
80+
with:
81+
version: 9
82+
- uses: actions/setup-node@v4
83+
with:
84+
node-version: '22'
85+
86+
# No `pnpm install` on purpose: the checker uses only Node builtins, the
87+
# repo's own scripts, `pnpm-lock.yaml` and `tar`. Skipping the install
88+
# keeps this job to a fetch and a hash.
89+
- name: Re-derive digests from the registry and diff against the pins
90+
run: pnpm check:binary-digests

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,6 @@
4949
**/PLAN.md
5050
packages/mcp-server/.chrome-profile/
5151
packages/mcp-server/dev-tools/
52-
packages/extension/README.md
52+
packages/extension/README.md
53+
packages/webview/.ds-css/
54+
packages/webview/.ds-src/

0 commit comments

Comments
 (0)