Skip to content

fix(mcp): take the outermost repository as the trust boundary (WALM-684) - #986

Merged
nikola0x0 merged 2 commits into
devfrom
nikolale/walm-684-mcp-launcher-treats-a-nested-package-as-the-repository
Sep 25, 2026
Merged

nikola0x0 merged 2 commits into
devfrom
nikolale/walm-684-mcp-launcher-treats-a-nested-package-as-the-repository

Conversation

@nikola0x0

Copy link
Copy Markdown
Collaborator

Closes WALM-684. Triage of the 22 Sep static security review (WALM-679); MW-05 confirmed against source.

What was wrong

enclosingProjectRoot stopped at the nearest ancestor carrying a .git or a package.json, so in a monorepo a client started in apps/web treated apps/web as the entire project. assertRuntimeRootLocation compares MEMWAL_MCP_RUNTIME_DIR against exactly that directory, so an override naming a sibling — <repo>/.memwal-runtime, apps/other/runtime — sat outside the nested package and passed, even though the same repository that supplied the override also controls the tree it points at.

That is the control the function exists to enforce: absoluteness is not trust, because a client expands ${workspaceFolder} to an absolute path.

The fix

Walk to the outermost marker instead of returning at the first. .git wins over package.json because it marks the checkout boundary rather than a workspace member; a package tree with no repository around it still counts, so a non-git monorepo keeps a boundary too. Continuing the walk past a match is also what lets a worktree's or submodule's .git file be found from inside a nested package.

This only ever rejects more. The default ~/.memwal/runtime is unaffected — home is still not a project — and a runtime genuinely outside the repository still resolves.

Verification

6 new tests: nested package boundary, sibling override refused from both the member and the repo root, legitimate external runtime still accepted, worktree .git file, no-git package tree, symlinked cwd. Full package suite 310 pass, 0 fail.


⚠️ Release blocker found while verifying MW-06 — please read before cutting the plugin

This is not fixed by this PR and needs a release decision. MW-06 (WALM-685) asks that what is published and installed be checked, not only what is merged to dev. Checking the actual published tarballs rather than inferring from dates:

Candidate SHA 06d75e2f (origin/dev)
packages/mcp version 0.0.14
Plugin version 0.0.14
Installed pin (mcpPackageVersion) 0.0.14-dev.2
Launch config .mcp.json, .codex-mcp.json, .cursor-mcp.json all run node ${PLUGIN_ROOT}/scripts/launch_mcp.mjs — the absolute launcher, so WALM-640 is in effect

0.0.14-dev.2 is missing consent.js and memory-policy.js — the WALM-642 secret-filtering and consent work. Its published dist/ carries 14 modules against 19 in src on dev. The pin was published before the WALM-642 merge landed, so installing the plugin from this candidate gives an MCP server without the secret-exclusion policy, even though dev has it.

  • 0.0.14-dev.6 contains all four missing modules.
  • 0.0.14-rc.0 contains three (no health-probe).
  • The existing guard accepts any prerelease of 0.0.14, so moving the pin is allowed.

Either move mcpPackageVersion to a build that contains those modules, or cut a fresh rc from current dev. I did not pick, since that is a release call.

Also note: the MW-05 fix in this PR is not in any published build yet, so a candidate cut before it merges still has the nested-package boundary gap.

All six fixes MW-06 lists were confirmed present on dev and absent on the reviewed main snapshot (0ffbacbb), which has no launcher file at all and still selects project credentials with no approval gate. dev is 173 commits ahead, 57 touching packages/mcp.

`enclosingProjectRoot` stopped at the nearest ancestor carrying a `.git`
*or* a `package.json`, so in a monorepo a client started in `apps/web`
treated `apps/web` as the entire project. `assertRuntimeRootLocation`
compares `MEMWAL_MCP_RUNTIME_DIR` against exactly that directory, so an
override naming a sibling — `<repo>/.memwal-runtime`, `apps/other/runtime` —
sat outside the nested package and passed, even though the same repository
that supplied the override also controls the tree it points at. That is the
control the function exists to enforce: absoluteness is not trust, because
a client expands `${workspaceFolder}` to an absolute path.

Walk to the outermost marker instead of returning at the first. `.git`
wins over `package.json` because it marks the checkout boundary rather
than a workspace member; a package tree with no repository around it still
counts, so a non-git monorepo keeps a boundary too. Continuing the walk
past a match is also what lets a worktree's or submodule's `.git` *file*
be found from inside a nested package.

This widens what gets refused, so it only ever rejects more. The default
`~/.memwal/runtime` is unaffected — home is still not a project — and a
runtime genuinely outside the repository still resolves.

Verified: 6 new tests (nested package boundary, sibling override refused
from both the member and the repo root, legitimate external runtime still
accepted, worktree `.git` file, no-git package tree, symlinked cwd).
Full package suite 310 pass, 0 fail.
@nikola0x0

Copy link
Copy Markdown
Collaborator Author

@ducnmm flagging you on the release blocker in the description above, since it is a release decision rather than something this PR can fix.

Short version: the plugin's mcpPackageVersion pins 0.0.14-dev.2, and that published build does not contain consent.js or memory-policy.js — the WALM-642 secret-filtering and consent work. I unpacked the tarball rather than going by dates: its dist/ has 14 modules against 19 in src on dev. The pin was published before the WALM-642 merge landed, so cutting the plugin from this candidate ships an MCP server without the secret-exclusion policy even though dev has it.

0.0.14-dev.6 has all four missing modules; 0.0.14-rc.0 has three (no health-probe). The pin guard accepts any prerelease of 0.0.14, so moving it is allowed — but picking between "move the pin to dev.6" and "cut a fresh rc from current dev" is yours, so I left it alone.

One more thing worth timing into that decision: the MW-05 launcher fix in this PR is not in any published build yet, so a candidate cut before this merges still carries the nested-package boundary gap.

Review follow-up on #986 (Harry). Walking to the *outermost* marker
introduced a regression the nearest-marker walk never had: a stray
`/package.json` or `/.git` — common in container images built with
WORKDIR / — outranked the real checkout below it, and a project of `/`
hits the `project === dirname(project)` branch, which returns null and
turns the location guard off entirely. From `/srv/app/packages/web` with
markers at `/` and `/srv/app`, the previous commit returned null; this
returns `/srv/app`.

The root is now excluded from the walk the same way home already was.
`enclosingProjectRoot` takes an injectable `exists` so the case can be
tested without writing to `/`.

Verified: two new tests (root marker above a real checkout; marker only at
root falls back to cwd, not to "no project"). Package suite passes.

@harrymove-ctrl harrymove-ctrl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Head identifies the outermost repository root (.git, then package.json) as the trust boundary rather than stopping at nested workspace packages. Fixes monorepo override escape. Tests and CI are green.

@nikola0x0
nikola0x0 merged commit ddfbb88 into dev Sep 25, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants