| """Structural (grep/AST) guards for the dedicated install flags. |
| |
| Cheap source-level guards that complement the behavioral tests: |
| |
| - Frontend 403 copy: both install surfaces in js/common.js name their |
| responsible flag, and the generic fallback copy stays unchanged. |
| - No new HTTP install surface is added. |
| - cm-cli stays an ungated local operator tool. |
| - The migration module never references the flags (no auto-seed — |
| explicit opt-in only). |
| |
| Harness: read/grep + AST over glob/*.py, cm-cli.py and js/*.js. No |
| imports of `glob/` modules (the dir name shadows stdlib glob). |
| """ |
| import re |
| import unittest |
| from pathlib import Path |
|
|
| REPO_ROOT = Path(__file__).resolve().parent.parent |
| MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py" |
| MANAGER_MIGRATION_PATH = REPO_ROOT / "glob" / "manager_migration.py" |
| CM_CLI_PATH = REPO_ROOT / "cm-cli.py" |
| JS_COMMON_PATH = REPO_ROOT / "js" / "common.js" |
|
|
| GENERIC_403_COPY = "This action is not allowed with this security level configuration." |
| FLAG_TOKENS = ("allow_git_url_install", "allow_pip_install") |
|
|
|
|
| def _js_function_block(source, func_name): |
| """Slice an `export async function <name>` block (up to the next |
| export or EOF).""" |
| start = source.find("export async function %s" % func_name) |
| if start < 0: |
| raise AssertionError("function %s not found in js source" % func_name) |
| next_export = source.find("export ", start + 1) |
| return source[start: next_export if next_export > 0 else len(source)] |
|
|
|
|
| def _handle403_call_args(source): |
| """All handle403Response(...) CALL argument strings (def/import lines |
| excluded).""" |
| calls = [] |
| for match in re.finditer(r"handle403Response\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)", source): |
| line_start = source.rfind("\n", 0, match.start()) + 1 |
| line = source[line_start: source.find("\n", match.start())] |
| if "function handle403Response" in line or line.lstrip().startswith("import"): |
| continue |
| calls.append(match.group(1).strip()) |
| return calls |
|
|
|
|
| class JsCopyStructuralTest(unittest.TestCase): |
| """Frontend honest-copy contract.""" |
|
|
| @classmethod |
| def setUpClass(cls): |
| cls.common_src = JS_COMMON_PATH.read_text(encoding="utf-8") |
|
|
| def test_surface_messages_name_their_flag(self): |
| """Both install 403 branches pass a flag-naming defaultMessage.""" |
| for func, flag in ( |
| ("install_via_git_url", "allow_git_url_install"), |
| ("install_pip", "allow_pip_install"), |
| ): |
| with self.subTest(func=func): |
| block = _js_function_block(self.common_src, func) |
| two_arg_calls = [a for a in _handle403_call_args(block) if "," in a] |
| self.assertTrue( |
| two_arg_calls, |
| "%s must call handle403Response with a defaultMessage" % func, |
| ) |
| self.assertIn(flag, block) |
| self.assertIn("config.ini", block) |
|
|
| def test_generic_fallback_and_frozen_callers_unchanged(self): |
| """The generic fallback copy stays (exactly its two occurrences in |
| handle403Response), and no other handle403Response caller across |
| js/ gains a defaultMessage.""" |
| |
| |
| |
| |
| handle_block = _js_function_block(self.common_src, "handle403Response") |
| self.assertEqual(handle_block.count(GENERIC_403_COPY), 2) |
| surface_blocks = "".join( |
| _js_function_block(self.common_src, name) |
| for name in ("install_pip", "install_via_git_url") |
| ) |
| allowed_two_arg = {a for a in _handle403_call_args(surface_blocks) if "," in a} |
| for js_file in sorted((REPO_ROOT / "js").glob("*.js")): |
| source = js_file.read_text(encoding="utf-8") |
| for args in _handle403_call_args(source): |
| if "," in args: |
| self.assertIn( |
| args, allowed_two_arg, |
| "frozen handle403Response caller in %s gained a " |
| "defaultMessage: handle403Response(%s)" % (js_file.name, args), |
| ) |
|
|
|
|
| class StructuralSecurityGuardsTest(unittest.TestCase): |
| """Source-level guards against scope bleed.""" |
|
|
| def test_no_new_install_route_surface(self): |
| """No new HTTP surface for git-URL/pip install.""" |
| source = MANAGER_SERVER_PATH.read_text(encoding="utf-8") |
| routes = set(re.findall(r"@routes\.post\(\"([^\"]+)\"\)", source)) |
| expected_surfaces = { |
| "/customnode/install/git_url", |
| "/customnode/install/pip", |
| "/manager/queue/install", |
| "/manager/queue/reinstall", |
| } |
| self.assertTrue(expected_surfaces.issubset(routes)) |
| install_like = {r for r in routes if "install" in r} |
| self.assertEqual( |
| install_like, |
| expected_surfaces |
| | {"/manager/queue/uninstall", "/manager/queue/install_model"}, |
| "install-like route set drifted — no new install surface allowed", |
| ) |
|
|
| def test_cm_cli_ungated(self): |
| """cm-cli stays a local operator tool — no gate, no flag lookup.""" |
| source = CM_CLI_PATH.read_text(encoding="utf-8") |
| for token in FLAG_TOKENS + ("is_allowed_security_level", "is_dedicated_install_allowed"): |
| self.assertNotIn(token, source, "cm-cli.py must stay ungated") |
|
|
| def test_no_autoseed_in_migration(self): |
| """The migration module never references the flags (explicit |
| opt-in only — no auto-seed from security_level).""" |
| source = MANAGER_MIGRATION_PATH.read_text(encoding="utf-8") |
| for token in FLAG_TOKENS: |
| self.assertNotIn( |
| token, source, |
| "manager_migration.py must not seed/translate the new flags", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main(verbosity=2) |
|
|