2 Commits

Author SHA1 Message Date
Mert Koseoglu 43b2477575 fix(integrity): algorithmic Algo-D4 — derive required siblings from scripts.bundle (closes #558 partial — 3 of 4)
v1.0.126 shipped Algo-D4 with a hardcoded REQUIRED_RUNTIME_SIBLINGS
array that omitted `hooks/security.bundle.mjs` (the bundle didn't
ship until v1.0.127, but the algorithmic intent was already
documented). The hardcoded list silently passed integrity checks on
v1.0.126 marketplace installs even when the security regression
was active — Algo-D4 reported `{ ok: true }` while permissions.deny
was fail-open. The same trap would have re-bitten the next bundle.

Algorithmic redesign:

- Replace `REQUIRED_RUNTIME_SIBLINGS` const with
  `getRequiredRuntimeSiblings(pluginRoot)` exported function.
- Algorithm: union of LEGACY_FALLBACK (the v1.0.126 contract,
  preserved verbatim) plus every esbuild outfile parsed from
  `package.json scripts.bundle` minus an explicit
  SOFT_FALLBACK_BUNDLES whitelist (session-* bundles, which have
  bundle-first/build-fallback in session-loaders.mjs and don't need
  to fail-fast).
- Source of truth: `scripts.bundle` `--outfile=` arguments. Adding
  a new bundle to that script auto-extends the integrity check —
  no parallel hardcoded list to maintain.
- Safety net: if package.json is unreadable, fall back to the
  legacy hardcoded set so the boot gate never goes silent.
- `assertPluginCacheIntegrity` now calls the new function. Public
  signature unchanged. start.mjs + the doctor surface are
  zero-touch — both consume the same algorithmically-derived set.

Tests (extend tests/core/cli.test.ts per CONTRIBUTING):
- "Algo-D4 algorithmically requires hooks/security.bundle.mjs" —
  the headline #558 regression: with security bundle missing on a
  fakeRoot, integrity must report ok=false (pre-558 hardcoded check
  vacuously passed).
- "Algo-D4 derivation reads scripts.bundle outfiles" — synthetic
  package.json proves a future hooks/foo.bundle.mjs is auto-gated,
  while soft-fallback session-db.bundle.mjs is correctly excluded.
- "Algo-D4 preserves the legacy hardcoded contract" — anti-
  regression pin: every entry in v1.0.126's hardcoded list is still
  in the algorithmic set. Strictly additive refactor.

Verified: 152/152 cli.test.ts tests pass (4 new Algo-D4 + 4
pre-existing plugin-cache + 144 unrelated). typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:30:50 +03:00
Mert Koseoglu df561af0a8 feat(start): plugin-cache integrity check derived from package.json files[] (algo defense 5 of 6)
#550: a partial install (interrupted npm install, broken marketplace
pull, half-finished /ctx-upgrade) leaves start.mjs spawnable but a
boot-critical sibling (server.bundle.mjs, cli.bundle.mjs,
hooks/<event>.mjs) missing. Today the MCP child dies silently
downstream — the user sees an opaque "MCP server failed to start" with
no actionable signal pointing at the missing files.

scripts/plugin-cache-integrity.mjs (new, ships in package.json files[])
exposes:

  - derivePluginManifest({ pkg, pluginRoot }) — reads files[] from the
    supplied pkg, expands directories recursively, returns the
    relative file list. Algorithmic: adding a new entry to files[]
    auto-extends manifest coverage. No parallel hardcoded list to
    maintain.

  - assertPluginCacheIntegrity({ pluginRoot }) — verifies each entry in
    a frozen REQUIRED_RUNTIME_SIBLINGS list (server.bundle.mjs,
    cli.bundle.mjs, the 5 hook scripts) exists. Pure: returns
    `{ ok, missing }` — the caller decides the surface (fail-fast at
    boot vs. doctor diagnostic).

  - formatPartialInstallReport({ pluginRoot, missing }) — the
    structured stderr block start.mjs emits on !ok. Marker
    `CONTEXT_MODE_PARTIAL_INSTALL` lets external monitoring grep for
    the exact failure mode.

start.mjs wiring: runs AFTER the existing heal layers (so missing
files they can fix get a chance first), BEFORE
`import("./server.bundle.mjs")`. On !ok, emits the structured report
and exits 2 instead of letting the downstream import surface the
opaque error. Skipped under VITEST so the repo's own test invocations
against in-tree start.mjs don't fail when running before
`npm run build` produces the bundles.

The helper is intentionally a separate `.mjs` (not src/util/*.ts) so
start.mjs (which ships as raw JS for cold-boot speed) can `await import`
it without a TS toolchain. The same `.mjs` is consumable by src/cli.ts
for D5's doctor surface.

15-adapter universality: start.mjs is the single MCP entry for every
adapter. One change here protects all 15.

Reproduce evidence (RED before GREEN):

  FAIL tests/core/cli.test.ts > start.mjs CLI self-heal > scripts/plugin-cache-integrity.mjs derives expected files from package.json files[]
  Error: Failed to resolve import "../../scripts/plugin-cache-integrity.mjs"

  FAIL tests/core/cli.test.ts > start.mjs CLI self-heal > start.mjs invokes assertPluginCacheIntegrity with stderr + exit 2 on failure (Algo-D4)
  AssertionError: expected start.mjs to contain "plugin-cache-integrity.mjs"

  FAIL tests/core/cli.test.ts > start.mjs CLI self-heal > scripts/plugin-cache-integrity.mjs ships in npm tarball (package.json files[])
  AssertionError: expected [ 'build', 'hooks', 'configs', …(21) ] to include 'scripts/plugin-cache-integrity.mjs'

5 RED tests, all GREEN post-fix. Full suite: 3199 pass / 8 baseline
opencode failures (unchanged). typecheck: PASS.

RED→GREEN: tests/core/cli.test.ts:1066-1156
2026-05-13 18:03:10 +03:00