4"""Require shell and GUI entry points to use the repository-owned Just launcher.
6Just recipes export the exact invoking executable through ``RA8_JUST``. Shell
7scripts then enter Just through ``scripts/dev/run_just.sh``, which preserves
8that executable even when a noninteractive PATH cannot find it. Direct calls
9inside a devcontainer command are a different namespace: the image owns its
10PATH and its pinned Just, so those argv tails remain bare by design.
12VS Code tasks, the IDE runbook, and the project MCP configuration are included
13because GUI processes commonly inherit a reduced PATH and must not bypass Just
14with raw script entry points. Workflow YAML remains outside this check: those
15calls are process entry points whose jobs provision Just before use. The sibling
16``check_justfiles.py`` owns recipe bodies and the devcontainer boundary.
19from __future__
import annotations
26from pathlib
import Path
28sys.path.insert(0, str(Path(__file__).resolve().parent))
30from shell_entrypoint_policy
import PRIVILEGED_PATHS, SHELL_POLICIES
31from shell_invocation_policy
import (
34from shell_invocation_policy
import (
35 selftest_failures
as structural_caller_selftest_failures,
38REPO_ROOT = Path(__file__).resolve().parents[2]
39SELF =
"scripts/checks/check_shell_just_invocations.py"
40VSCODE_TASKS =
".vscode/tasks.json"
41IDE_DOCUMENT =
"docs/IDE.md"
42MCP_CONFIG =
".mcp.json"
43ROOT_JUSTFILE =
"justfile"
44WORKSPACE_JUSTFILE =
"just/ws.just"
45SENSITIVE_BOUNDARY_LINES = (
48 ((
'["/bin/bash", "-p", "scripts/ci.sh", "--list-gates"],', 1),),
51 "scripts/ci/check_ci_parity.py",
52 ((
'["/bin/bash", "-p", str(CI_SH), "--list-gates"],', 1),),
57 (
"#!/bin/bash -p", 4),
59 "# SHEBANG-SECURITY: -p blocks BASH_ENV and exported-function startup injection.",
62 (
'/bin/bash -p scripts/dev/openocd_flash.sh "$hex"', 1),
63 (
'/bin/bash -p scripts/dev/flash.sh "$hex"', 1),
64 (
'/bin/bash -p scripts/dev/openocd_debug.sh "$elf"', 1),
65 (
'/bin/bash -p scripts/dev/debug.sh "$elf"', 1),
66 (
'/bin/bash -p scripts/dev/ozone.sh "$elf"', 1),
67 (
"/bin/bash -p scripts/dev/monitor.sh", 1),
70 (
"just/emu.just", ((
"/bin/bash -p scripts/emu/setup_macos.sh", 1),)),
72 "scripts/emu/setup_macos.sh",
74 (
'/bin/bash -p "${installer}"', 1),
76 'RA8_UNICORN_PREFIX="$prefix" /bin/bash -p "$root/scripts/ci/install_unicorn.sh"',
82 "infra/network/verify_bench_wifi.sh",
85 'setsid /bin/bash -p -c "sleep ${RESTORE_AFTER}; ${RESTORE_CMD}" '
86 ">/dev/null 2>&1 </dev/null &",
92 "scripts/builders/docs.sh",
93 ((
'DOXYGEN_BIN="$(/bin/bash -p "${SCRIPT_DIR}/provision_doxygen.sh")"', 1),),
96 "scripts/ci/gates/checks.sh",
97 ((
"/bin/bash -p scripts/ci/devcontainer_image.sh --selftest-offline", 1),),
100 "scripts/ci/gates/hygiene.sh",
102 (
"/bin/bash -p scripts/dev/setup_python.sh --selftest", 1),
103 (
"/bin/bash -p scripts/ci/monitor.sh selftest", 1),
107 "scripts/ci/lib/container.sh",
108 ((
'/bin/bash -p "$repo/scripts/ci/devcontainer_image.sh" "${args[@]}"', 1),),
111 "scripts/dev/remote_gdb_server.sh",
113 (
'/bin/bash -p "$ROOT/scripts/hil/flash.sh" "$APP_ID"', 1),
114 (
'REMOTE_START_COMMAND="$(', 1),
115 (
'"$PYTHON" -I "$ARGS_GUARD" remote-command \\', 1),
116 (
'-- "$PI_HOST" "$REMOTE_START_COMMAND" <"$REMOTE_GUARD" &', 1),
117 (
'REMOTE_CLEANUP_COMMAND="$(', 0),
118 (
'"$PYTHON" -I "$ARGS_GUARD" remote-command cleanup \\', 0),
119 (
'"$PYTHON" -I "$ARGS_GUARD" remote-command start \\', 0),
120 (
'/usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -- "$PI_HOST" \\', 0),
121 (
"\"$REMOTE_CLEANUP_COMMAND\" <<'REMOTE_CLEANUP'", 0),
122 (
'-- "$PI_HOST" "$REMOTE_START_COMMAND" <<\'REMOTE\' &', 0),
126 "scripts/dev/remote_gdb_args.py",
129 'return shlex.join(["/usr/bin/python3", "-I", "-", "--", *fields])',
132 (
'return shlex.join(["/bin/bash", "-p", "-s", "--", *fields])', 0),
137 ((
"/bin/bash -p scripts/builders/publish_docs.sh", 1),),
142 "docs/sbom/upstream/",
144 "apps/shared_libs/third_party/",
152MIN_PROTECTED_SCRIPTS = 65
153MIN_CALLER_FILES = 650
154PROTECTED_SHEBANG =
"#!/bin/bash -p"
155PROTECTED_REASON =
"# SHEBANG-SECURITY: -p blocks BASH_ENV and exported-function startup injection."
156FUTURE_SCRIPT_FIXTURE =
"scripts/secrets/future_ceremony.sh"
159 r"(?:^\s*|(?:&&|\|\||;|\|)\s*|\$\(\s*)"
160 r"(?:if\s+|elif\s+|while\s+|until\s+)?!?(?:\s*)"
161 r"(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+)\s+)*"
162 r"(?:(?:exec|time)\s+)?"
164BARE_JUST_RE = re.compile(COMMAND_PREFIX +
r"just(?=\s|$)")
165ARRAY_JUST_RE = re.compile(
r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\(\s*(?:[\"'])?just(?:[\"'])?(?=\s|\))")
166SHELL_COMMAND_RE = re.compile(
167 r"\b(?:ba|z|da)?sh\s+(?:-[A-Za-z]*c|--command)\s+([\"'])\s*just(?=\s)"
169COMMAND_SUB_JUST_RE = re.compile(
r"\$\(\s*just(?=\s|$)")
170HEREDOC_RE = re.compile(
r"<<-?\s*([\"']?)([A-Za-z_][A-Za-z0-9_]*)\1")
171SCRIPT_PATH_RE = re.compile(
172 r"(?:\$\{workspaceFolder\}/)?(?P<path>scripts/[A-Za-z0-9_./-]+\.(?:sh|py))"
174UNPRIVILEGED_RUN_JUST_RE = re.compile(
175 r"(?<![A-Za-z0-9_/])bash\s+(?:\"[^\"]*\"|'[^']*'|[^\s;|&]*)scripts/dev/run_just\.sh"
179def _is_shell(path: Path, text: str) -> bool:
180 """Return whether ``path`` is a first-party shell entry point."""
181 if path.suffix
in {
".sh",
".bash",
".zsh"}:
183 first = text.partition(
"\n")[0]
184 return first.startswith(
"#!")
and re.search(
r"\b(?:ba|z|da)?sh\b", first)
is not None
187def scoped_files() -> list[str]:
188 """Return tracked and untracked first-party shell entry points."""
189 git_bin = shutil.which(
"git")
or "git"
190 proc = subprocess.run(
191 [git_bin,
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"-z"],
197 for rel
in proc.stdout.decode(
"utf-8", errors=
"strict").split(
"\0"):
198 if not rel
or rel.startswith(EXCLUDED_PREFIXES):
200 path = REPO_ROOT / rel
201 if not path.is_file():
204 text = path.read_text(encoding=
"utf-8")
205 except UnicodeDecodeError:
207 if _is_shell(path, text):
209 return sorted(set(rels))
212def bare_just_line(line: str) -> bool:
213 """Return whether one active shell line launches Just through PATH."""
214 stripped = line.lstrip()
215 if not stripped
or stripped.startswith(
"#"):
217 if "devcontainer_run.sh" in line
and re.search(
r"--\s+.*\bjust\b", line)
is not None:
219 masked = _mask_quotes(line)
221 pattern.search(candidate)
is not None
222 for pattern, candidate
in (
223 (BARE_JUST_RE, masked),
224 (ARRAY_JUST_RE, line),
225 (SHELL_COMMAND_RE, line),
226 (COMMAND_SUB_JUST_RE, _mask_single_quotes(line)),
231def _mask_quotes(line: str) -> str:
232 """Mask quoted prose while retaining token width for shell assignments."""
233 masked: list[str] = []
241 elif char ==
"\\" and quote ==
'"':
250 return "".join(masked)
253def _mask_single_quotes(line: str) -> str:
254 """Mask single-quoted literals, where command substitution is inactive."""
255 masked: list[str] = []
259 in_single =
not in_single
265 return "".join(masked)
268def scan_text(text: str) -> list[int]:
269 """Return line numbers containing bare Just commands outside heredocs."""
270 findings: list[int] = []
271 heredoc_end: str |
None =
None
272 lines = text.splitlines()
274 while index < len(lines):
278 if heredoc_end
is not None:
279 if line.strip() == heredoc_end:
283 while logical.rstrip().endswith(
"\\")
and index < len(lines):
284 logical = logical.rstrip()[:-1] +
" " + lines[index].lstrip()
286 if bare_just_line(logical)
or UNPRIVILEGED_RUN_JUST_RE.search(logical):
287 findings.append(number)
288 match = HEREDOC_RE.search(logical)
289 if match
is not None:
290 heredoc_end = match.group(2)
294def scan(rels: list[str]) -> list[str]:
295 """Return every shell, editor, and MCP entry-point bypass finding."""
296 findings: list[str] = []
298 text = (REPO_ROOT / rel).read_text(encoding=
"utf-8")
299 findings.extend(f
"{rel}:{line}" for line
in scan_text(text))
301 findings.extend(_fixed_surface_findings())
302 protected = protected_script_paths(rels)
303 if len(protected) < MIN_PROTECTED_SCRIPTS:
305 f
"protected shell population collapsed to {len(protected)}; "
306 f
"expected at least {MIN_PROTECTED_SCRIPTS}"
308 if protected != PRIVILEGED_PATHS:
310 f
"typed/header protected population drift: {path}"
311 for path
in sorted(protected ^ PRIVILEGED_PATHS)
313 findings.extend(scan_structural_callers(rels))
314 findings.extend(scan_sensitive_boundary_files())
318def _fixed_surface_findings() -> list[str]:
319 """Validate required VS Code, IDE, MCP, and fixed Just surfaces."""
320 findings: list[str] = []
321 tasks_path = REPO_ROOT / VSCODE_TASKS
322 if not tasks_path.is_file():
323 findings.append(f
"{VSCODE_TASKS}: required GUI task configuration is missing")
325 task_findings = scan_vscode_tasks(tasks_path.read_text(encoding=
"utf-8"))
326 findings.extend(f
"{VSCODE_TASKS}: {finding}" for finding
in task_findings)
328 ide_path = REPO_ROOT / IDE_DOCUMENT
329 if not ide_path.is_file():
330 findings.append(f
"{IDE_DOCUMENT}: required IDE runbook is missing")
333 f
"{IDE_DOCUMENT}:{line}: launches a raw script instead of run_just.sh"
334 for line
in scan_ide_document(ide_path.read_text(encoding=
"utf-8"))
337 mcp_path = REPO_ROOT / MCP_CONFIG
338 if not mcp_path.is_file():
339 findings.append(f
"{MCP_CONFIG}: required project MCP configuration is missing")
341 mcp_findings = scan_mcp_config(mcp_path.read_text(encoding=
"utf-8"))
342 findings.extend(f
"{MCP_CONFIG}: {finding}" for finding
in mcp_findings)
343 root_just = REPO_ROOT / ROOT_JUSTFILE
344 if not root_just.is_file():
345 findings.append(f
"{ROOT_JUSTFILE}: required root Justfile is missing")
347 root_findings = scan_root_justfile(root_just.read_text(encoding=
"utf-8"))
348 findings.extend(f
"{ROOT_JUSTFILE}: {finding}" for finding
in root_findings)
349 workspace_just = REPO_ROOT / WORKSPACE_JUSTFILE
350 if not workspace_just.is_file():
351 findings.append(f
"{WORKSPACE_JUSTFILE}: required workspace Justfile is missing")
353 workspace_findings = scan_workspace_justfile(workspace_just.read_text(encoding=
"utf-8"))
354 findings.extend(f
"{WORKSPACE_JUSTFILE}: {finding}" for finding
in workspace_findings)
358def is_protected_script_text(text: str) -> bool:
359 """Return whether a shell entry owns the exact reviewed startup boundary."""
360 return text.splitlines()[:4] == [
362 "# SPDX-License-Identifier: MIT",
363 "# Copyright (c) 2026 Brighton Sikarskie",
368def protected_script_paths(rels: list[str]) -> set[str]:
369 """Derive the protected script population from exact reviewed file headers."""
370 protected: set[str] = set()
372 path = REPO_ROOT / rel
374 text = path.read_text(encoding=
"utf-8")
375 except (OSError, UnicodeError):
377 if is_protected_script_text(text):
382def caller_files(shell_rels: list[str]) -> list[str]:
383 """Return all first-party executable and configuration caller surfaces."""
384 git_bin = shutil.which(
"git")
or "git"
385 proc = subprocess.run(
386 [git_bin,
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"-z"],
391 callers = set(shell_rels)
392 for rel
in proc.stdout.decode(
"utf-8", errors=
"strict").split(
"\0"):
393 if not rel
or rel.startswith(EXCLUDED_PREFIXES):
397 rel ==
".env.example"
398 or path.name ==
"justfile"
399 or path.name.endswith(
"Dockerfile")
411 return sorted(callers)
414def scan_structural_callers(shell_rels: list[str]) -> list[str]:
415 """Validate all supported first-party caller formats structurally."""
416 findings: list[str] = []
417 callers = caller_files(shell_rels)
418 if len(callers) < MIN_CALLER_FILES:
420 f
"structural caller scope collapsed to {len(callers)}; "
421 f
"expected at least {MIN_CALLER_FILES}"
424 path = REPO_ROOT / rel
426 text = path.read_text(encoding=
"utf-8")
427 except (OSError, UnicodeError):
430 f
"{rel}:{finding.line}: {finding.message}"
431 for finding
in scan_caller_text(rel, text, SHELL_POLICIES)
436def scan_root_justfile(text: str) -> list[str]:
437 """Require fixed Bash at setup and writable-container boundaries."""
439 "/bin/bash -p scripts/ci/devcontainer_image.sh ensure",
440 "/bin/bash -p scripts/dev/setup_python.sh setup",
441 "/bin/bash -p scripts/dev/setup_ansible.sh",
442 "/bin/bash -p scripts/ci/devcontainer_run.sh -- /bin/bash -p",
443 "/usr/bin/env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin "
444 "/bin/bash -p -c 'if [[ -x /usr/bin/nproc ]]",
445 "$(/usr/bin/git config core.hooksPath)",
448 f
"root recipe lost exact privileged entry: {line}" for line
in required
if line
not in text
452def scan_workspace_justfile(text: str) -> list[str]:
453 """Require fixed Bash for workspace lifecycle and monitor boundaries."""
455 "/bin/bash -p scripts/dev/agent_workspace.sh create",
456 "/bin/bash -p scripts/dev/agent_workspace.sh release",
457 "/bin/bash -p scripts/dev/agent_workspace.sh list",
458 "/bin/bash -p scripts/dev/agent_workspace.sh doctor",
459 "/bin/bash -p scripts/dev/agent_workspace.sh reap",
460 "/bin/bash -p scripts/ci/monitor.sh status",
461 "/bin/bash -p scripts/ci/monitor.sh quota",
464 f
"workspace recipe lost exact privileged entry: {line}"
468 multiline_recipes = (
"new",
"reap",
"status")
469 if text.count(
"#!/bin/bash -p") != len(multiline_recipes):
470 findings.append(
"workspace multiline recipes must own exactly three privileged shebangs")
474def scan_sensitive_boundary_text(rel: str, text: str) -> list[str]:
475 """Require exact protected argv at hardware, installer, and nested boundaries."""
477 (required
for path, required
in SENSITIVE_BOUNDARY_LINES
if path == rel),
482 lines = [line.strip()
for line
in text.splitlines()
if line.strip()]
484 f
"{rel}: expected {count} exact occurrence(s) of {line!r}, found {lines.count(line)}"
485 for line, count
in contract
486 if lines.count(line) != count
490def scan_sensitive_boundary_files() -> list[str]:
491 """Scan every fixed sensitive caller, failing closed when one disappears."""
492 findings: list[str] = []
493 for rel, _required
in SENSITIVE_BOUNDARY_LINES:
494 path = REPO_ROOT / rel
495 if not path.is_file():
496 findings.append(f
"{rel}: required sensitive boundary is missing")
498 findings.extend(scan_sensitive_boundary_text(rel, path.read_text(encoding=
"utf-8")))
502def _direct_just_command(command: object) -> bool:
503 """Return whether a VS Code task command directly selects Just."""
504 if not isinstance(command, str):
506 first = command.strip().split(maxsplit=1)[0]
if command.strip()
else ""
507 return first ==
"just" or first.endswith(
"/just")
510def _raw_script_paths(values: list[object]) -> list[str]:
511 """Return first-party script paths other than the canonical launcher."""
512 paths: list[str] = []
514 if not isinstance(value, str):
516 for match
in SCRIPT_PATH_RE.finditer(value):
517 path = match.group(
"path")
518 if path !=
"scripts/dev/run_just.sh":
523def scan_vscode_tasks(text: str) -> list[str]:
524 """Return labels of VS Code tasks that bypass ``run_just.sh``."""
526 document = json.loads(text)
527 except json.JSONDecodeError
as exc:
528 return [f
"invalid JSON ({exc.msg})"]
529 if not isinstance(document, dict)
or not isinstance(document.get(
"tasks"), list):
530 return [
"top-level tasks array is missing"]
531 findings: list[str] = []
532 for index, task
in enumerate(document[
"tasks"], start=1):
533 if not isinstance(task, dict):
534 findings.append(f
"task {index} is not an object")
536 label = task.get(
"label", f
"task {index}")
537 if _direct_just_command(task.get(
"command")):
538 findings.append(f
"task {label!r} launches Just directly")
540 args = task.get(
"args", [])
541 values = [task.get(
"command"), *(args
if isinstance(args, list)
else [])]
542 paths = _raw_script_paths(values)
544 findings.append(f
"task {label!r} launches raw script {paths[0]!r}")
545 elif any(_is_just_launcher(value)
for value
in values)
and not _valid_vscode_launcher(
546 task.get(
"command"), args
548 findings.append(f
"task {label!r} misplaces scripts/dev/run_just.sh")
552def scan_ide_document(text: str) -> list[int]:
553 """Return IDE runbook lines that invoke first-party scripts directly."""
556 for number, line
in enumerate(text.splitlines(), start=1)
557 if _raw_script_paths([line])
561def _is_just_launcher(value: object) -> bool:
562 """Return whether one argv value names the repository Just launcher."""
563 return isinstance(value, str)
and value.replace(
"\\",
"/").endswith(
"scripts/dev/run_just.sh")
566def _valid_vscode_launcher(command: object, args: object) -> bool:
567 """Require the launcher in executable position, followed by a recipe."""
568 if not isinstance(args, list)
or not args:
571 command ==
"/bin/bash"
572 and len(args) >= MIN_LAUNCHER_ARGV
574 and _is_just_launcher(args[1])
575 and isinstance(args[2], str)
580def scan_mcp_config(text: str) -> list[str]:
581 """Return project MCP servers that do not enter through the MCP Just recipe."""
583 document = json.loads(text)
584 except json.JSONDecodeError
as exc:
585 return [f
"invalid JSON ({exc.msg})"]
586 servers = document.get(
"mcpServers")
if isinstance(document, dict)
else None
587 if not isinstance(servers, dict)
or not servers:
588 return [
"top-level mcpServers object is missing or empty"]
589 findings: list[str] = []
590 for name, server
in servers.items():
591 if not isinstance(server, dict):
592 findings.append(f
"server {name!r} is not an object")
594 args = server.get(
"args")
595 if server.get(
"command") !=
"/bin/bash" or args != [
597 "scripts/dev/run_just.sh",
601 f
"server {name!r} must use exact /bin/bash -p, run_just.sh, MCP recipe argv"
606def _shell_selftest_failures() -> tuple[list[str], int]:
607 """Exercise shell parsing in both directions."""
609 (
"just ci\n", [1],
"direct command fires"),
610 (
"if just apps::build blink; then :; fi\n", [1],
"conditional command fires"),
611 (
"CC=clang just tools::build\n", [1],
"environment-prefixed command fires"),
612 (
"false || just hil::probe\n", [1],
"chained command fires"),
613 (
"cmd=(just quality::run)\n", [1],
"deferred command array fires"),
614 (
"bash -uc 'just tests::build'\n", [1],
"nested shell command fires"),
615 (
'bash "$root/scripts/dev/run_just.sh" ci\n', [1],
"unprivileged launcher fires"),
617 '/bin/bash -p "$root/scripts/dev/run_just.sh" ci\n',
619 "privileged launcher stays quiet",
622 "bash scripts/ci/devcontainer_run.sh -- just quality::local::test\n",
624 "container-owned command stays quiet",
627 "exec bash scripts/ci/devcontainer_run.sh -- \\\n just quality::local::test\n",
629 "continued container-owned command stays quiet",
631 (
'version="$(just --version)"\n', [1],
"unresolved version probe fires"),
632 (
'echo "run just ci"\n', [],
"help prose stays quiet"),
633 (
"cat <<'HELP'\ncd repo && just ci\nHELP\n", [],
"heredoc guidance stays quiet"),
635 return [label
for text, expected, label
in cases
if scan_text(text) != expected], len(cases)
638def _task_selftest_failures() -> tuple[list[str], int]:
639 """Exercise VS Code entry-point parsing in both directions."""
641 (
'{"tasks":[{"label":"bad","command":"just"}]}', 1,
"bare GUI command fires"),
643 '{"tasks":[{"label":"bad","command":"/usr/local/bin/just"}]}',
645 "absolute GUI command fires",
648 '{"tasks":[{"label":"safe","command":"/bin/bash",'
649 '"args":["-p","scripts/dev/run_just.sh","ci"]}]}',
651 "GUI launcher call stays quiet",
654 '{"tasks":[{"label":"bad","command":"/bin/bash",'
655 '"args":["scripts/dev/run_just.sh","ci"]}]}',
657 "GUI launcher without privileged mode fires",
660 '{"tasks":[{"label":"bad","command":"scripts/dev/run_just.sh","args":["ci"]}]}',
662 "direct GUI launcher fires",
665 '{"tasks":[{"label":"bad","command":"bash","args":["scripts/hil/flash.sh"]}]}',
667 "raw GUI script fires",
670 f
'{{"tasks":[{{"label":"bad","command":"bash","args":["{FUTURE_SCRIPT_FIXTURE}"]}}]}}',
672 "future raw GUI script fires",
675 '{"tasks":[{"label":"bad","command":"/bin/echo",'
676 '"args":["scripts/dev/run_just.sh","ci"]}]}',
678 "misplaced GUI launcher fires",
682 label
for text, expected, label
in task_cases
if len(scan_vscode_tasks(text)) != expected
684 return failures, len(task_cases)
687def _ide_selftest_failures() -> tuple[list[str], int]:
688 """Exercise IDE runbook entry-point parsing in both directions."""
690 (
"GDB args: scripts/dev/remote_gdb_server.sh run\n", [1],
"raw IDE script fires"),
692 "GDB args: scripts/dev/run_just.sh hil::remote_gdb run\n",
694 "IDE launcher stays quiet",
697 f
"GDB args: {FUTURE_SCRIPT_FIXTURE} run\n",
699 "future raw IDE script fires",
702 failures = [label
for text, expected, label
in ide_cases
if scan_ide_document(text) != expected]
703 return failures, len(ide_cases)
706def _mcp_selftest_failures() -> tuple[list[str], int]:
707 """Exercise exact MCP executable and argv parsing in both directions."""
710 '{"mcpServers":{"bad":{"command":"python3","args":["server.py"]}}}',
712 "raw MCP command fires",
715 f
'{{"mcpServers":{{"bad":{{"command":"bash","args":["{FUTURE_SCRIPT_FIXTURE}"]}}}}',
717 "future raw MCP script fires",
720 '{"mcpServers":{"safe":{"command":"/bin/bash",'
721 '"args":["-p","scripts/dev/run_just.sh","tools::mcp_server"]}}}',
723 "MCP launcher stays quiet",
726 '{"mcpServers":{"bad":{"command":"/bin/bash",'
727 '"args":["scripts/dev/run_just.sh","tools::mcp_server"]}}}',
729 "MCP launcher without privileged mode fires",
732 '{"mcpServers":{"bad":{"command":"/bin/bash",'
733 '"args":["scripts/dev/run_just.sh","tools::mcp"]}}}',
735 "wrong MCP recipe fires",
738 '{"mcpServers":{"bad":{"command":"/bin/echo",'
739 '"args":["scripts/dev/run_just.sh","tools::mcp_server"]}}}',
741 "misplaced MCP launcher fires",
744 '{"mcpServers":{"bad":{"command":"/bin/bash",'
745 '"args":["tools::mcp_server","scripts/dev/run_just.sh"]}}}',
747 "reordered MCP argv fires",
751 label
for text, expected, label
in mcp_cases
if len(scan_mcp_config(text)) != expected
753 return failures, len(mcp_cases)
756def _root_just_selftest_failures() -> tuple[list[str], int]:
757 """Exercise fixed root-recipe Bash ownership in both directions."""
759 "/bin/bash -p scripts/ci/devcontainer_image.sh ensure\n"
760 "/bin/bash -p scripts/dev/setup_python.sh setup\n"
761 "/bin/bash -p scripts/dev/setup_ansible.sh\n"
762 "/bin/bash -p scripts/ci/devcontainer_run.sh -- /bin/bash -p\n"
763 "/usr/bin/env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin "
764 "/bin/bash -p -c 'if [[ -x /usr/bin/nproc ]]\n"
765 "$(/usr/bin/git config core.hooksPath)\n"
768 (safe, 0,
"privileged root recipes stay quiet"),
769 (safe.replace(
"/bin/bash -p",
"bash", 1), 1,
"PATH Bash in root recipe fires"),
770 (safe.replace(
" -- /bin/bash -p",
" -- bash"), 1,
"PATH inner shell fires"),
772 safe.replace(
"/usr/bin/env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin",
"env"),
774 "caller environment in CPU probe fires",
777 safe.replace(
"/usr/bin/git config",
"git config"),
779 "PATH Git in hook status fires",
783 label
for text, expected, label
in cases
if len(scan_root_justfile(text)) != expected
785 return failures, len(cases)
788def _workspace_just_selftest_failures() -> tuple[list[str], int]:
789 """Exercise exact workspace lifecycle and monitor entry points."""
791 "#!/bin/bash -p\n#!/bin/bash -p\n#!/bin/bash -p\n"
792 "/bin/bash -p scripts/dev/agent_workspace.sh create\n"
793 "/bin/bash -p scripts/dev/agent_workspace.sh release\n"
794 "/bin/bash -p scripts/dev/agent_workspace.sh list\n"
795 "/bin/bash -p scripts/dev/agent_workspace.sh doctor\n"
796 "/bin/bash -p scripts/dev/agent_workspace.sh reap\n"
797 "/bin/bash -p scripts/ci/monitor.sh status\n"
798 "/bin/bash -p scripts/ci/monitor.sh quota\n"
801 (safe, 0,
"privileged workspace recipes stay quiet"),
803 safe.replace(
"/bin/bash -p scripts/ci/monitor.sh",
"bash scripts/ci/monitor.sh", 1),
805 "PATH Bash monitor entry fires",
809 "/bin/bash -p scripts/dev/agent_workspace.sh",
810 "bash scripts/dev/agent_workspace.sh",
814 "PATH Bash lifecycle entry fires",
816 (safe.replace(
"#!/bin/bash -p",
"#!/usr/bin/env bash", 1), 1,
"PATH shebang fires"),
819 label
for text, expected, label
in cases
if len(scan_workspace_justfile(text)) != expected
821 return failures, len(cases)
824def _sensitive_boundary_selftest_failures() -> tuple[list[str], int]:
825 """Exercise every exact nested boundary in both directions."""
826 failures: list[str] = []
828 for rel, requirements
in SENSITIVE_BOUNDARY_LINES:
829 safe_lines = [line
for line, copies
in requirements
for _
in range(copies)]
830 safe =
"\n".join(safe_lines) +
"\n"
832 if scan_sensitive_boundary_text(rel, safe):
833 failures.append(f
"exact sensitive boundary {rel} was rejected")
834 if "/bin/bash -p" in safe:
835 weakened = safe.replace(
"/bin/bash -p",
"bash", 1)
836 elif '"/usr/bin/python3", "-I"' in safe:
837 weakened = safe.replace(
'"/usr/bin/python3", "-I"',
'"python3"', 1)
839 weakened = safe.replace(
'"/bin/bash", "-p"',
'"bash"', 1)
840 if not scan_sensitive_boundary_text(rel, weakened):
841 failures.append(f
"weakened sensitive boundary {rel} was accepted")
842 duplicated = safe + requirements[0][0] +
"\n"
843 if not scan_sensitive_boundary_text(rel, duplicated):
844 failures.append(f
"duplicated sensitive boundary {rel} was accepted")
846 if scan_sensitive_boundary_text(
"scripts/emu/matrix.sh",
"bash ordinary.sh\n"):
847 failures.append(
"ordinary emulator wrapper was pulled into the sensitive registry")
849 contracts = dict(SENSITIVE_BOUNDARY_LINES)
850 server_rel =
"scripts/dev/remote_gdb_server.sh"
851 args_rel =
"scripts/dev/remote_gdb_args.py"
853 "\n".join(line
for line, copies
in contracts[server_rel]
for _
in range(copies)) +
"\n"
855 old_cleanup = server_safe +
'REMOTE_CLEANUP_COMMAND="$(\n'
856 if not scan_sensitive_boundary_text(server_rel, old_cleanup):
857 failures.append(
"obsolete remote PID-sweep authority was accepted")
858 heredoc = server_safe.replace(
859 '-- "$PI_HOST" "$REMOTE_START_COMMAND" <"$REMOTE_GUARD" &',
860 '-- "$PI_HOST" "$REMOTE_START_COMMAND" <<\'REMOTE\' &',
862 if not scan_sensitive_boundary_text(server_rel, heredoc):
863 failures.append(
"inline remote heredoc replaced the fixed supervisor payload")
865 "\n".join(line
for line, copies
in contracts[args_rel]
for _
in range(copies)) +
"\n"
867 reordered = args_safe.replace(
868 '["/usr/bin/python3", "-I", "-", "--", *fields]',
869 '["/usr/bin/python3", "-I", "-", *fields, "--"]',
871 if not scan_sensitive_boundary_text(args_rel, reordered):
872 failures.append(
"reordered remote supervisor argv was accepted")
874 return failures, count
877def _structural_caller_selftest_failures() -> tuple[list[str], int]:
878 """Exercise the format-aware caller authority and header derivation."""
879 failures, count = structural_caller_selftest_failures()
882 f
"{PROTECTED_SHEBANG}\n"
883 "# SPDX-License-Identifier: MIT\n"
884 "# Copyright (c) 2026 Brighton Sikarskie\n"
885 f
"{PROTECTED_REASON}\n"
888 f
"{PROTECTED_SHEBANG}\n{PROTECTED_REASON}\n"
889 "# SPDX-License-Identifier: MIT\n"
890 "# Copyright (c) 2026 Brighton Sikarskie\n"
892 (f
"{PROTECTED_SHEBANG}\n# SPDX-License-Identifier: MIT\n{PROTECTED_REASON}\n"),
893 "#!/usr/bin/env bash\n# ordinary entry\n",
895 if [is_protected_script_text(text)
for text
in headers] != [
True,
False,
False,
False]:
896 failures.append(
"protected script population derivation is not two-sided")
897 return failures, count + len(headers)
900def _entrypoint_selftest_failures() -> tuple[list[str], int]:
901 """Combine VS Code, IDE, and MCP entry-point tests."""
903 _task_selftest_failures(),
904 _ide_selftest_failures(),
905 _mcp_selftest_failures(),
906 _root_just_selftest_failures(),
907 _workspace_just_selftest_failures(),
908 _sensitive_boundary_selftest_failures(),
909 _structural_caller_selftest_failures(),
911 return [failure
for failures, _count
in groups
for failure
in failures], sum(
912 count
for _failures, count
in groups
916def selftest() -> int:
917 """Prove shell and GUI entry-point checks in both directions."""
918 shell_failures, shell_count = _shell_selftest_failures()
919 entry_failures, entry_count = _entrypoint_selftest_failures()
920 failures = shell_failures + entry_failures
921 for failure
in failures:
922 print(f
"check_shell_just_invocations.py --selftest: FAIL: {failure}", file=sys.stderr)
925 print(f
"check_shell_just_invocations.py --selftest: PASS ({shell_count + entry_count} cases)")
930 """Run detector self-tests or scan the live first-party shell scope."""
931 if sys.argv[1:] == [
"--selftest"]:
934 print(
"usage: check_shell_just_invocations.py [--selftest]", file=sys.stderr)
937 rels = scoped_files()
938 except (OSError, subprocess.CalledProcessError, UnicodeError)
as exc:
939 print(f
"cannot enumerate first-party shell files: {exc}", file=sys.stderr)
941 if len(rels) < MIN_SHELL_FILES
or not (REPO_ROOT / SELF).is_file():
943 f
"shell scope collapsed to {len(rels)} files; expected at least "
944 f
"{MIN_SHELL_FILES} with checker {SELF}",
948 findings = scan(rels)
950 print(
"shell or GUI entry point(s) bypass scripts/dev/run_just.sh:", file=sys.stderr)
951 for finding
in findings:
952 print(f
" {finding}", file=sys.stderr)
954 print(f
"check_shell_just_invocations.py: clean ({len(rels)} shell files + GUI entry points)")
958if __name__ ==
"__main__":
959 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.