fix(plugins): keep dashboard/ and JS module files revision-owned on no-git carry

A subdirectory install has no .git, so the carry cannot tell removed
upstream code from user files and relies on a deny-list. That list missed
the dashboard surface (web_server_dashboard loads dashboard/manifest.json)
and .mjs/.cjs/.jsx/.tsx, so an upstream that dropped its dashboard or a
hook script got the old files back.

Add dashboard/ to _NO_GIT_REVISION_DIRS and the four extensions as a
carry-local set. They stay out of tools.plugin_guard.CODE_FILE_EXTENSIONS
on purpose: that set exempts code files from env-secret scan patterns, so
widening it there would weaken scanning rather than broaden it.

The docs now list what is actually enforced, and the existing subdir
update test pins dashboard/manifest.json + hooks/run.cjs removed upstream
are not resurrected.
This commit is contained in:
kshitijk4poor
2026-09-26 21:36:45 +05:30
committed by kshitij
parent 3d5ad396fc
commit 1362b95c04
3 changed files with 19 additions and 4 deletions

View File

@@ -289,7 +289,10 @@ _NO_GIT_REVISION_FILES = frozenset({
"plugin.yaml", "plugin.yml", "plugin.json", "mcp.json",
"pyproject.toml", "package.json", "package-lock.json", "uv.lock",
})
_NO_GIT_REVISION_DIRS = frozenset({"desktop", "skills", "sidecar", "node_modules"})
_NO_GIT_REVISION_DIRS = frozenset({"dashboard", "desktop", "skills", "sidecar", "node_modules"})
# JS module/JSX variants the guard does not classify as code. Kept local: adding them to
# tools.plugin_guard.CODE_FILE_EXTENSIONS would exempt them from env-secret scan patterns.
_NO_GIT_REVISION_EXTENSIONS = frozenset({".mjs", ".cjs", ".jsx", ".tsx"})
def _skip_preserve(name: str) -> bool:
@@ -300,8 +303,10 @@ def _skip_preserve(name: str) -> bool:
def _revision_owned_without_git(rel: Path) -> bool:
"""True for plugin code/control surfaces an update must never resurrect from the old tree."""
from tools.plugin_guard import CODE_FILE_EXTENSIONS
suffix = rel.suffix.lower()
return (
rel.suffix.lower() in CODE_FILE_EXTENSIONS
suffix in CODE_FILE_EXTENSIONS
or suffix in _NO_GIT_REVISION_EXTENSIONS
or rel.as_posix() in _NO_GIT_REVISION_FILES
or bool(rel.parts and rel.parts[0] in _NO_GIT_REVISION_DIRS)
)

View File

@@ -216,6 +216,10 @@ def test_update_of_a_subdir_install_keeps_files_the_user_created_or_edited(world
(src / "desktop").mkdir()
(src / "desktop" / "plugin.js").write_text("export default { id: \"v1\" }\n")
(src / "server.js").write_text("console.log('v1')\n")
(src / "dashboard").mkdir()
(src / "dashboard" / "manifest.json").write_text("{}")
(src / "hooks").mkdir()
(src / "hooks" / "run.cjs").write_text("module.exports = 1\n")
sp.run(["git", "init", "-q"], cwd=mono, check=True, env=_GIT_ENV)
pin = {"sha": _commit(mono, "v1")}
@@ -244,6 +248,8 @@ def test_update_of_a_subdir_install_keeps_files_the_user_created_or_edited(world
(src / "config.yaml.example").write_text("endpoint: new-default\n")
shutil.rmtree(src / "desktop")
(src / "server.js").unlink()
shutil.rmtree(src / "dashboard")
shutil.rmtree(src / "hooks")
pin["sha"] = _commit(mono, "v2")
assert pc.dashboard_update_user_plugin("sub-plugin")["ok"] is True
@@ -252,6 +258,7 @@ def test_update_of_a_subdir_install_keeps_files_the_user_created_or_edited(world
assert (target / "data" / "state.json").read_text() == "{}"
assert not (target / "desktop").exists()
assert not (target / "server.js").exists()
assert not (target / "dashboard").exists() and not (target / "hooks" / "run.cjs").exists()
def test_repin_keeps_a_wholly_ignored_data_dir_in_a_git_checkout(world):

View File

@@ -198,8 +198,11 @@ enabled/disabled state is preserved, and so are files the plugin's repo does
not track (the `config.yaml` created from its `.example`, data files, `.env`).
For monorepo/subdirectory installs, which do not carry a local Git checkout,
update preserves user-state files the new revision does not ship. Plugin code and
control surfaces (Python, Desktop/skills, manifests/MCP and dependency metadata)
remain revision-owned and are not resurrected from the old install. If a user-state
control surfaces remain revision-owned and are not resurrected from the old install:
source files (Python, JavaScript/TypeScript including `.mjs`/`.cjs`/`.jsx`/`.tsx`,
shell, Ruby, Perl, PHP), the top-level `dashboard/`, `desktop/`, `skills/`,
`sidecar/` and `node_modules/` directories, the plugin manifest, `mcp.json` and
dependency metadata (`pyproject.toml`, `package.json`, lockfiles). If a user-state
path conflicts with the new tree's file/directory layout, the update stops before
publication so the installed copy — and the user's data — remain intact.
Edits you made to *tracked* files are not carried onto the new code; copies are