personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_schedule_config_static.py
5.4 kB 155 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Static guard for direct durable writes to config/schedules.json. 5 6Known limitation: this detects direct durable-write sinks to a schedules path, not 7writes routed through a generic helper that receives the path as an opaque 8parameter. That indirect case is closed by the journal_io access gate and the 9raw-mechanic gate, which together require any new writer to surface. 10""" 11 12from __future__ import annotations 13 14import ast 15from pathlib import Path 16 17ROOT = Path(__file__).resolve().parents[1] 18WRITE_PRIMITIVES = { 19 "atomic_replace", 20 "install_file", 21 "write_json", 22 "write_jsonl", 23 "write_text", 24} 25WRITE_MODE_CHARS = frozenset({"w", "a", "x", "+"}) 26 27 28def _production_modules() -> list[Path]: 29 return sorted( 30 path 31 for path in (ROOT / "solstone").rglob("*.py") 32 if path.is_file() 33 and "__pycache__" not in path.parts 34 and "tests" not in path.parts 35 and path.relative_to(ROOT).as_posix() != "solstone/think/schedule_config.py" 36 ) 37 38 39def _call_name(node: ast.AST) -> str | None: 40 if isinstance(node, ast.Name): 41 return node.id 42 if isinstance(node, ast.Attribute): 43 return node.attr 44 return None 45 46 47def _is_schedules_path_expr(node: ast.AST, schedules_vars: set[str]) -> bool: 48 if isinstance(node, ast.Name): 49 return node.id in schedules_vars 50 if isinstance(node, ast.Call): 51 return _call_name(node.func) == "get_schedules_path" 52 if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): 53 return ( 54 isinstance(node.right, ast.Constant) 55 and node.right.value == "schedules.json" 56 ) 57 return False 58 59 60def _is_write_mode(node: ast.AST | None) -> bool: 61 if node is None: 62 return False 63 if isinstance(node, ast.Constant) and isinstance(node.value, str): 64 return any(char in node.value for char in WRITE_MODE_CHARS) 65 return False 66 67 68def _open_mode(call: ast.Call) -> ast.AST | None: 69 if len(call.args) >= 2: 70 return call.args[1] 71 for keyword in call.keywords: 72 if keyword.arg == "mode": 73 return keyword.value 74 return None 75 76 77class SchedulesWriteVisitor(ast.NodeVisitor): 78 def __init__(self, path: Path) -> None: 79 self.path = path 80 self.schedules_vars: set[str] = set() 81 self.violations: list[str] = [] 82 83 def visit_Assign(self, node: ast.Assign) -> None: 84 if _is_schedules_path_expr(node.value, self.schedules_vars): 85 for target in node.targets: 86 if isinstance(target, ast.Name): 87 self.schedules_vars.add(target.id) 88 self.generic_visit(node) 89 90 def visit_AnnAssign(self, node: ast.AnnAssign) -> None: 91 if ( 92 node.value is not None 93 and isinstance(node.target, ast.Name) 94 and _is_schedules_path_expr(node.value, self.schedules_vars) 95 ): 96 self.schedules_vars.add(node.target.id) 97 self.generic_visit(node) 98 99 def visit_Call(self, node: ast.Call) -> None: 100 self._check_open(node) 101 self._check_path_open(node) 102 self._check_write_primitive(node) 103 self._check_replace(node) 104 self.generic_visit(node) 105 106 def _record(self, node: ast.AST, kind: str) -> None: 107 rel = self.path.relative_to(ROOT) 108 self.violations.append(f"{rel}:{node.lineno}: {kind}") 109 110 def _check_open(self, node: ast.Call) -> None: 111 if _call_name(node.func) != "open" or not node.args: 112 return 113 if _is_schedules_path_expr( 114 node.args[0], self.schedules_vars 115 ) and _is_write_mode(_open_mode(node)): 116 self._record(node, "open-write") 117 118 def _check_path_open(self, node: ast.Call) -> None: 119 if not isinstance(node.func, ast.Attribute) or node.func.attr != "open": 120 return 121 if _is_schedules_path_expr( 122 node.func.value, self.schedules_vars 123 ) and _is_write_mode(_open_mode(node)): 124 self._record(node, "Path.open-write") 125 126 def _check_write_primitive(self, node: ast.Call) -> None: 127 if _call_name(node.func) not in WRITE_PRIMITIVES or not node.args: 128 return 129 if _is_schedules_path_expr(node.args[0], self.schedules_vars): 130 self._record(node, _call_name(node.func) or "write-primitive") 131 132 def _check_replace(self, node: ast.Call) -> None: 133 if isinstance(node.func, ast.Attribute) and node.func.attr == "replace": 134 if len(node.args) >= 2 and _is_schedules_path_expr( 135 node.args[1], self.schedules_vars 136 ): 137 self._record(node, "os.replace") 138 return 139 if node.args and _is_schedules_path_expr(node.args[0], self.schedules_vars): 140 self._record(node, "Path.replace") 141 return 142 if _call_name(node.func) == "replace" and len(node.args) >= 2: 143 if _is_schedules_path_expr(node.args[1], self.schedules_vars): 144 self._record(node, "os.replace") 145 146 147def test_only_schedule_config_directly_writes_schedules_json() -> None: 148 violations: list[str] = [] 149 for path in _production_modules(): 150 tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) 151 visitor = SchedulesWriteVisitor(path) 152 visitor.visit(tree) 153 violations.extend(visitor.violations) 154 155 assert violations == []