Something went wrong. Try again.
personal memory agent
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248#!/usr/bin/env python3# SPDX-License-Identifier: AGPL-3.0-only# Copyright (c) 2026 sol pbc
"""Built-in ``sol call`` tools HTTP-only lint."""
from __future__ import annotations
import argparseimport astimport sysfrom pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TARGETS: tuple[str, ...] = ( "solstone/think/tools/health.py", "solstone/think/tools/ledger.py", "solstone/think/tools/profile.py",)ALLOW_SET: frozenset[str] = frozenset( { "solstone.think.convey_client", "solstone.convey.reasons", "solstone.convey.readiness_snapshot", "solstone.think.pipeline_health", })FLAGGED_NAMESPACES: tuple[str, ...] = ( "solstone.think", "solstone.apps", "solstone.convey",)IO_METHODS: frozenset[str] = frozenset( { "read_text", "read_bytes", "write_text", "write_bytes", "open", "unlink", "mkdir", "rmdir", "touch", "glob", "iterdir", "rglob", })OS_FS_FUNCS: frozenset[str] = frozenset( { "remove", "unlink", "mkdir", "makedirs", "rmdir", "removedirs", "rename", "renames", "replace", "symlink", "link", "listdir", "scandir", "walk", "chmod", "chown", "truncate", "mkfifo", "mknod", "open", })SHUTIL_FS_FUNCS: frozenset[str] = frozenset( { "move", "copy", "copy2", "copyfile", "copytree", "rmtree", "copyfileobj", "make_archive", "unpack_archive", "copymode", "copystat", "chown", })
def _is_under_namespace(module: str) -> bool: return any( module == namespace or module.startswith(f"{namespace}.") for namespace in FLAGGED_NAMESPACES )
def _collect_bindings( tree: ast.AST,) -> tuple[set[str], dict[str, str], set[str], dict[str, str]]: os_aliases: set[str] = set() os_direct_names: dict[str, str] = {} shutil_aliases: set[str] = set() shutil_direct_names: dict[str, str] = {}
for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: bound = alias.asname or alias.name if alias.name == "os": os_aliases.add(bound) elif alias.name == "shutil": shutil_aliases.add(bound) elif isinstance(node, ast.ImportFrom): module = node.module or "" for alias in node.names: bound = alias.asname or alias.name if module == "os" and alias.name in OS_FS_FUNCS: os_direct_names[bound] = alias.name elif module == "shutil" and alias.name in SHUTIL_FS_FUNCS: shutil_direct_names[bound] = alias.name
return os_aliases, os_direct_names, shutil_aliases, shutil_direct_names
def _scan_import(node: ast.Import | ast.ImportFrom) -> list[tuple[int, str, str]]: findings: list[tuple[int, str, str]] = [] if isinstance(node, ast.Import): for alias in node.names: module = alias.name if _is_under_namespace(module) and module not in ALLOW_SET: findings.append((node.lineno, "import", module)) return findings
module = node.module or "" if not _is_under_namespace(module): return findings if module in ALLOW_SET: return findings if len(node.names) == 1 and f"{module}.{node.names[0].name}" in ALLOW_SET: return findings return [(node.lineno, "import", module)]
def _scan_call( node: ast.Call, os_aliases: set[str], os_direct_names: dict[str, str], shutil_aliases: set[str], shutil_direct_names: dict[str, str],) -> tuple[int, str, str] | None: func = node.func
if isinstance(func, ast.Name): if func.id == "open": return node.lineno, "fs", "open" if func.id in os_direct_names: return node.lineno, "fs", f"os.{os_direct_names[func.id]}" if func.id in shutil_direct_names: return node.lineno, "fs", f"shutil.{shutil_direct_names[func.id]}" return None
if not isinstance(func, ast.Attribute): return None
if func.attr in IO_METHODS: return node.lineno, "fs", f"Path.{func.attr}"
if ( isinstance(func.value, ast.Name) and func.value.id in os_aliases and func.attr in OS_FS_FUNCS ): return node.lineno, "fs", f"os.{func.attr}"
if ( isinstance(func.value, ast.Name) and func.value.id in shutil_aliases and func.attr in SHUTIL_FS_FUNCS ): return node.lineno, "fs", f"shutil.{func.attr}"
return None
def scan_source(source: str, filename: str = "<source>") -> list[tuple[int, str, str]]: tree = ast.parse(source, filename=filename) os_aliases, os_direct_names, shutil_aliases, shutil_direct_names = ( _collect_bindings(tree) )
findings: list[tuple[int, str, str]] = [] for node in ast.walk(tree): if isinstance(node, (ast.Import, ast.ImportFrom)): findings.extend(_scan_import(node)) elif isinstance(node, ast.Call): finding = _scan_call( node, os_aliases, os_direct_names, shutil_aliases, shutil_direct_names, ) if finding: findings.append(finding)
findings.sort() return findings
def scan_file(path: Path) -> list[tuple[int, str, str]]: return scan_source(path.read_text(encoding="utf-8"), filename=str(path))
def discover_modules(root: Path) -> list[Path]: return [Path(target) for target in TARGETS if (root / target).is_file()]
def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description="built-in sol call tools HTTP-only lint" ) parser.add_argument( "--root", type=Path, default=ROOT, help="Repository root to scan (defaults to the checkout root).", ) args = parser.parse_args(argv)
findings: list[str] = [] for rel in discover_modules(args.root): for lineno, kind, detail in scan_file(args.root / rel): findings.append(f"{rel.as_posix()}:{lineno}: {kind} {detail}")
if findings: for finding in findings: print(finding, file=sys.stderr) return 1
print("tools-http-only: pass") return 0
if __name__ == "__main__": raise SystemExit(main())