watchllm-kernel 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. watchllm_kernel-0.1.0/PKG-INFO +138 -0
  2. watchllm_kernel-0.1.0/README.md +114 -0
  3. watchllm_kernel-0.1.0/pyproject.toml +48 -0
  4. watchllm_kernel-0.1.0/setup.cfg +4 -0
  5. watchllm_kernel-0.1.0/src/watchllm_kernel/__init__.py +23 -0
  6. watchllm_kernel-0.1.0/src/watchllm_kernel/__main__.py +4 -0
  7. watchllm_kernel-0.1.0/src/watchllm_kernel/cli.py +214 -0
  8. watchllm_kernel-0.1.0/src/watchllm_kernel/config_loader.py +43 -0
  9. watchllm_kernel-0.1.0/src/watchllm_kernel/engine.py +115 -0
  10. watchllm_kernel-0.1.0/src/watchllm_kernel/models.py +100 -0
  11. watchllm_kernel-0.1.0/src/watchllm_kernel/parser.py +128 -0
  12. watchllm_kernel-0.1.0/src/watchllm_kernel/reporting.py +186 -0
  13. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/__init__.py +17 -0
  14. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/_ast_utils.py +96 -0
  15. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/auth_flow.py +300 -0
  16. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/boundary.py +231 -0
  17. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/forbidden_imports.py +202 -0
  18. watchllm_kernel-0.1.0/src/watchllm_kernel/rules/secrets.py +190 -0
  19. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/PKG-INFO +138 -0
  20. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/SOURCES.txt +34 -0
  21. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/dependency_links.txt +1 -0
  22. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/entry_points.txt +2 -0
  23. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/requires.txt +4 -0
  24. watchllm_kernel-0.1.0/src/watchllm_kernel.egg-info/top_level.txt +1 -0
  25. watchllm_kernel-0.1.0/tests/test_auth_flow_rule.py +93 -0
  26. watchllm_kernel-0.1.0/tests/test_benchmarks.py +82 -0
  27. watchllm_kernel-0.1.0/tests/test_boundary_rule.py +108 -0
  28. watchllm_kernel-0.1.0/tests/test_cli.py +173 -0
  29. watchllm_kernel-0.1.0/tests/test_cli_contract.py +146 -0
  30. watchllm_kernel-0.1.0/tests/test_e2e.py +212 -0
  31. watchllm_kernel-0.1.0/tests/test_engine.py +153 -0
  32. watchllm_kernel-0.1.0/tests/test_fixture_corpus.py +57 -0
  33. watchllm_kernel-0.1.0/tests/test_forbidden_import_rule.py +115 -0
  34. watchllm_kernel-0.1.0/tests/test_models.py +115 -0
  35. watchllm_kernel-0.1.0/tests/test_parser.py +94 -0
  36. watchllm_kernel-0.1.0/tests/test_reporting.py +132 -0
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchllm-kernel
3
+ Version: 0.1.0
4
+ Summary: Deterministic local runtime governance kernel for autonomous coding agents.
5
+ Author: WatchLLM
6
+ License: Apache-2.0
7
+ Keywords: watchllm,kernel,runtime-governance,agent-governance,static-analysis
8
+ Classifier: Development Status :: 1 - Planning
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Security
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: tree-sitter>=0.21.0
21
+ Requires-Dist: tree-sitter-javascript>=0.21.0
22
+ Requires-Dist: tree-sitter-typescript>=0.21.0
23
+ Requires-Dist: pyyaml>=6.0.0
24
+
25
+ # WatchLLM Kernel
26
+
27
+ ```
28
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
29
+ ░ ░░░░ ░░░ ░░░ ░░░ ░░░ ░░░░ ░░ ░░░░░░░░ ░░░░░░░░ ░░░░ ░
30
+ ▒ ▒ ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ ▒
31
+ ▓ ▓▓ ▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓
32
+ █ ██ ██ █████ █████ ████ ██ ████ ██ ████████ ████████ █ █ █
33
+ █ ████ ██ ████ █████ ██████ ███ ████ ██ ██ ██ ████ █
34
+ ████████████████████████████████████████████████████████████████████████████████
35
+ ```
36
+
37
+ Deterministic local write-path governance kernel for autonomous coding agents.
38
+
39
+ ## Current status
40
+
41
+ Task 14 complete — core model layer, parser abstraction, fixture corpus, rule implementations, deterministic decision engine, CLI evaluation interface, end-to-end regression tests, baseline performance benchmarks, and local blocked-event reporting exist. Save-path editor integration is not implemented yet.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ python -m pip install -e .
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ watchllm-kernel --help
53
+ python -m watchllm_kernel --help
54
+ ```
55
+
56
+ ## Fixture corpus
57
+
58
+ Rule evidence fixtures live under `tests/fixtures/rules/`.
59
+
60
+ Each MVP rule category has a minimal `pass/` and `fail/` fixture set:
61
+
62
+ - `secrets`
63
+ - `forbidden_imports`
64
+ - `boundary`
65
+ - `auth_flow`
66
+
67
+ These fixtures are rule evidence examples and are used by rule-specific tests as each rule is implemented.
68
+
69
+ ## Implemented rules
70
+
71
+ ### Secret-literal rule
72
+
73
+ The secret-literal rule detects hardcoded credential patterns in assignment contexts and dangerous call contexts. It uses AST context to avoid flagging safe retrieval calls such as `process.env.STRIPE_SECRET` or `os.getenv("STRIPE_SECRET")`.
74
+
75
+ ### Forbidden-import rule
76
+
77
+ The forbidden-import rule blocks dangerous imports such as `child_process` and disallowed relative traversal imports. It extracts ES module imports and CommonJS `require(...)` calls using AST traversal rather than raw text scanning.
78
+
79
+ ### Boundary rule
80
+
81
+ The boundary rule checks AST-extracted import edges against a small declared boundary map. In the current policy, `auth` may import the public DB contract but must not import `db/internal` paths directly.
82
+
83
+ Circular dependency detection is explicitly deferred because Task 08 evaluates single-file import edges only, not a repository-wide import graph.
84
+
85
+ ### Auth-flow rule
86
+
87
+ The auth-flow rule checks calls inside an exported `handler` function and requires an explicit auth guard before protected database operations such as `db.user.update(...)`.
88
+
89
+ Current Task 09 behaviour is intentionally narrow:
90
+
91
+ - mutation before auth returns `FAIL`
92
+ - auth before mutation returns `PASS`
93
+ - auth found only inside an ambiguous branch before mutation returns `INCONCLUSIVE`
94
+
95
+ Repository-wide control-flow analysis is not implemented yet.
96
+
97
+ ## Decision engine
98
+
99
+ The decision engine runs a supplied ordered list of rules against one source buffer and reduces their rule results into one `KernelResult`.
100
+
101
+ In enforce mode, any rule failure produces `BLOCK`.
102
+
103
+ In shadow mode, rule failures are preserved in the result but the final decision remains `ALLOW`.
104
+
105
+ For Task 10, `INCONCLUSIVE` rule results are recorded but do not block.
106
+
107
+ ## Benchmarks
108
+
109
+ Run the current Python kernel benchmark suite with:
110
+
111
+ ```bash
112
+ python benchmarks/run_benchmarks.py --iterations 50 --warmup 5 --json
113
+ ```
114
+
115
+ Benchmark baseline documentation lives in `docs/benchmarks/baseline.md`.
116
+
117
+ ## Local violation reporting
118
+
119
+ Blocked evaluations are written locally as JSONL.
120
+
121
+ Default path:
122
+
123
+ ```bash
124
+ .watchllm/logs/violations.jsonl
125
+ ```
126
+
127
+ Override path:
128
+
129
+ ```bash
130
+ WATCHLLM_LOG_PATH=/tmp/watchllm-violations.jsonl python -m watchllm_kernel evaluate path/to/file.ts --json
131
+ ```
132
+
133
+ The reporting contract is documented in `docs/specs/reporting-contract.md`.
134
+
135
+ ## Non‑goals (current state)
136
+
137
+ - No save-path editor integration yet
138
+ - No cloud dependency or network enforcement
@@ -0,0 +1,114 @@
1
+ # WatchLLM Kernel
2
+
3
+ ```
4
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
5
+ ░ ░░░░ ░░░ ░░░ ░░░ ░░░ ░░░░ ░░ ░░░░░░░░ ░░░░░░░░ ░░░░ ░
6
+ ▒ ▒ ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒ ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ ▒
7
+ ▓ ▓▓ ▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓
8
+ █ ██ ██ █████ █████ ████ ██ ████ ██ ████████ ████████ █ █ █
9
+ █ ████ ██ ████ █████ ██████ ███ ████ ██ ██ ██ ████ █
10
+ ████████████████████████████████████████████████████████████████████████████████
11
+ ```
12
+
13
+ Deterministic local write-path governance kernel for autonomous coding agents.
14
+
15
+ ## Current status
16
+
17
+ Task 14 complete — core model layer, parser abstraction, fixture corpus, rule implementations, deterministic decision engine, CLI evaluation interface, end-to-end regression tests, baseline performance benchmarks, and local blocked-event reporting exist. Save-path editor integration is not implemented yet.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ python -m pip install -e .
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ watchllm-kernel --help
29
+ python -m watchllm_kernel --help
30
+ ```
31
+
32
+ ## Fixture corpus
33
+
34
+ Rule evidence fixtures live under `tests/fixtures/rules/`.
35
+
36
+ Each MVP rule category has a minimal `pass/` and `fail/` fixture set:
37
+
38
+ - `secrets`
39
+ - `forbidden_imports`
40
+ - `boundary`
41
+ - `auth_flow`
42
+
43
+ These fixtures are rule evidence examples and are used by rule-specific tests as each rule is implemented.
44
+
45
+ ## Implemented rules
46
+
47
+ ### Secret-literal rule
48
+
49
+ The secret-literal rule detects hardcoded credential patterns in assignment contexts and dangerous call contexts. It uses AST context to avoid flagging safe retrieval calls such as `process.env.STRIPE_SECRET` or `os.getenv("STRIPE_SECRET")`.
50
+
51
+ ### Forbidden-import rule
52
+
53
+ The forbidden-import rule blocks dangerous imports such as `child_process` and disallowed relative traversal imports. It extracts ES module imports and CommonJS `require(...)` calls using AST traversal rather than raw text scanning.
54
+
55
+ ### Boundary rule
56
+
57
+ The boundary rule checks AST-extracted import edges against a small declared boundary map. In the current policy, `auth` may import the public DB contract but must not import `db/internal` paths directly.
58
+
59
+ Circular dependency detection is explicitly deferred because Task 08 evaluates single-file import edges only, not a repository-wide import graph.
60
+
61
+ ### Auth-flow rule
62
+
63
+ The auth-flow rule checks calls inside an exported `handler` function and requires an explicit auth guard before protected database operations such as `db.user.update(...)`.
64
+
65
+ Current Task 09 behaviour is intentionally narrow:
66
+
67
+ - mutation before auth returns `FAIL`
68
+ - auth before mutation returns `PASS`
69
+ - auth found only inside an ambiguous branch before mutation returns `INCONCLUSIVE`
70
+
71
+ Repository-wide control-flow analysis is not implemented yet.
72
+
73
+ ## Decision engine
74
+
75
+ The decision engine runs a supplied ordered list of rules against one source buffer and reduces their rule results into one `KernelResult`.
76
+
77
+ In enforce mode, any rule failure produces `BLOCK`.
78
+
79
+ In shadow mode, rule failures are preserved in the result but the final decision remains `ALLOW`.
80
+
81
+ For Task 10, `INCONCLUSIVE` rule results are recorded but do not block.
82
+
83
+ ## Benchmarks
84
+
85
+ Run the current Python kernel benchmark suite with:
86
+
87
+ ```bash
88
+ python benchmarks/run_benchmarks.py --iterations 50 --warmup 5 --json
89
+ ```
90
+
91
+ Benchmark baseline documentation lives in `docs/benchmarks/baseline.md`.
92
+
93
+ ## Local violation reporting
94
+
95
+ Blocked evaluations are written locally as JSONL.
96
+
97
+ Default path:
98
+
99
+ ```bash
100
+ .watchllm/logs/violations.jsonl
101
+ ```
102
+
103
+ Override path:
104
+
105
+ ```bash
106
+ WATCHLLM_LOG_PATH=/tmp/watchllm-violations.jsonl python -m watchllm_kernel evaluate path/to/file.ts --json
107
+ ```
108
+
109
+ The reporting contract is documented in `docs/specs/reporting-contract.md`.
110
+
111
+ ## Non‑goals (current state)
112
+
113
+ - No save-path editor integration yet
114
+ - No cloud dependency or network enforcement
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "watchllm-kernel"
7
+ version = "0.1.0"
8
+ description = "Deterministic local runtime governance kernel for autonomous coding agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [
13
+ { name = "WatchLLM" }
14
+ ]
15
+ keywords = [
16
+ "watchllm",
17
+ "kernel",
18
+ "runtime-governance",
19
+ "agent-governance",
20
+ "static-analysis"
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 1 - Planning",
24
+ "Environment :: Console",
25
+ "Intended Audience :: Developers",
26
+ "License :: OSI Approved :: Apache Software License",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: Software Development :: Quality Assurance",
32
+ "Topic :: Security"
33
+ ]
34
+ dependencies = [
35
+ "tree-sitter>=0.21.0",
36
+ "tree-sitter-javascript>=0.21.0",
37
+ "tree-sitter-typescript>=0.21.0",
38
+ "pyyaml>=6.0.0",
39
+ ]
40
+
41
+ [project.scripts]
42
+ watchllm-kernel = "watchllm_kernel.cli:main"
43
+
44
+ [tool.setuptools]
45
+ package-dir = {"" = "src"}
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from watchllm_kernel.models import (
4
+ Decision,
5
+ KernelResult,
6
+ Rule,
7
+ RuleDecision,
8
+ RuleResult,
9
+ Severity,
10
+ SourceLocation,
11
+ Violation,
12
+ )
13
+
14
+ __all__ = [
15
+ "Decision",
16
+ "KernelResult",
17
+ "Rule",
18
+ "RuleDecision",
19
+ "RuleResult",
20
+ "Severity",
21
+ "SourceLocation",
22
+ "Violation",
23
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,214 @@
1
+ import argparse
2
+ import dataclasses
3
+ import enum
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from watchllm_kernel.engine import ENFORCE_MODE, SHADOW_MODE, evaluate_source
10
+ from watchllm_kernel.models import Decision
11
+ from watchllm_kernel.reporting import format_human_report, write_block_log
12
+ from watchllm_kernel.rules.auth_flow import AuthFlowRule
13
+ from watchllm_kernel.rules.boundary import BoundaryRule
14
+ from watchllm_kernel.rules.forbidden_imports import ForbiddenImportRule
15
+ from watchllm_kernel.config_loader import load_config
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Helpers
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ def _to_jsonable(obj: Any) -> Any:
24
+ """Convert dataclasses and enums to JSON‑serialisable primitives."""
25
+ if dataclasses.is_dataclass(obj):
26
+ return {f.name: _to_jsonable(getattr(obj, f.name)) for f in dataclasses.fields(obj)}
27
+ if isinstance(obj, enum.Enum):
28
+ return obj.value
29
+ if isinstance(obj, list):
30
+ return [_to_jsonable(item) for item in obj]
31
+ return obj
32
+
33
+
34
+ def build_default_rules(config: dict | None = None):
35
+ """Return the default rule set for the kernel CLI, applying overrides from config.
36
+ """
37
+ config = config or {}
38
+ rules = []
39
+
40
+ try:
41
+ from watchllm_kernel.rules.secrets import SecretLiteralRule
42
+ except ModuleNotFoundError:
43
+ SecretLiteralRule = None
44
+
45
+ if SecretLiteralRule is not None:
46
+ secrets_cfg = config.get("rules", {}).get("secrets", {})
47
+ if secrets_cfg.get("enabled", True):
48
+ rules.append(SecretLiteralRule())
49
+
50
+ # Build Forbidden Import Rule
51
+ fi_cfg = config.get("rules", {}).get("forbidden_imports", {})
52
+ if fi_cfg.get("enabled", True):
53
+ rules.append(ForbiddenImportRule(
54
+ forbidden_modules=fi_cfg.get("modules"),
55
+ forbidden_prefixes=fi_cfg.get("forbidden_prefixes"),
56
+ allowed_relative_prefixes=fi_cfg.get("allowed_relative_prefixes")
57
+ ))
58
+
59
+ # Build Boundary Rule
60
+ boundary_cfg = config.get("rules", {}).get("boundary", {})
61
+ if boundary_cfg.get("enabled", True):
62
+ rules.append(BoundaryRule(
63
+ boundary_map=boundary_cfg.get("map")
64
+ ))
65
+
66
+ # Build Auth Flow Rule
67
+ auth_cfg = config.get("rules", {}).get("auth_flow", {})
68
+ if auth_cfg.get("enabled", True):
69
+ rules.append(AuthFlowRule())
70
+
71
+ return rules
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Argument parser
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ def build_parser() -> argparse.ArgumentParser:
80
+ parser = argparse.ArgumentParser(
81
+ prog="watchllm-kernel",
82
+ description="Deterministic local write-path governance kernel for autonomous coding agents.",
83
+ )
84
+ parser.add_argument(
85
+ "--version",
86
+ action="version",
87
+ version="%(prog)s 0.1.0",
88
+ )
89
+
90
+ sub = parser.add_subparsers(dest="command", help="sub-command")
91
+
92
+ # check
93
+ check_parser = sub.add_parser("check", help="Check source against rules")
94
+ check_parser.add_argument(
95
+ "--stdin",
96
+ action="store_true",
97
+ help="Read source from stdin instead of a file path",
98
+ )
99
+ check_parser.add_argument(
100
+ "--filepath",
101
+ default=None,
102
+ help="Path to source file (ignored when --stdin is used)",
103
+ )
104
+ check_parser.add_argument(
105
+ "--language",
106
+ choices=["js", "ts"],
107
+ default=None,
108
+ help="Language identifier (js or ts). Inferred from file extension when omitted.",
109
+ )
110
+ check_parser.add_argument(
111
+ "--mode",
112
+ choices=[ENFORCE_MODE, SHADOW_MODE],
113
+ default=ENFORCE_MODE,
114
+ help="Evaluation mode (default: enforce)",
115
+ )
116
+ check_parser.add_argument(
117
+ "--json",
118
+ action="store_true",
119
+ help="Output result as JSON",
120
+ )
121
+ return parser
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Language resolution
126
+ # ---------------------------------------------------------------------------
127
+
128
+ _LANGUAGE_SHORT_MAP = {
129
+ "js": "javascript",
130
+ "ts": "typescript",
131
+ }
132
+
133
+
134
+ def _resolve_language(language: str | None, file_path: str | None) -> str | None:
135
+ if language and language in _LANGUAGE_SHORT_MAP:
136
+ return _LANGUAGE_SHORT_MAP[language]
137
+ if language:
138
+ return language
139
+ if file_path is None:
140
+ return None
141
+ suffix = Path(file_path).suffix.lower()
142
+ if suffix in (".ts", ".tsx"):
143
+ return "typescript"
144
+ if suffix in (".js", ".jsx", ".mjs", ".cjs"):
145
+ return "javascript"
146
+ return None
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Main
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ def main(argv: list[str] | None = None) -> int:
155
+ parser = build_parser()
156
+ if argv is None:
157
+ argv = sys.argv[1:]
158
+
159
+ if not argv:
160
+ parser.print_help()
161
+ return 0
162
+
163
+ args = parser.parse_args(argv)
164
+
165
+ if args.command != "check":
166
+ parser.print_help()
167
+ return 0
168
+
169
+ # --- read source ---
170
+ if args.stdin:
171
+ source = sys.stdin.read()
172
+ file_path = None
173
+ else:
174
+ if args.filepath is None:
175
+ print("Error: either --stdin or --filepath is required", file=sys.stderr)
176
+ return 2
177
+ file_path = args.filepath
178
+ try:
179
+ source = Path(file_path).read_text(encoding="utf-8")
180
+ except Exception as exc:
181
+ print(f"Error reading file {file_path}: {exc}", file=sys.stderr)
182
+ return 2
183
+
184
+ language = _resolve_language(args.language, file_path)
185
+
186
+ # --- load config ---
187
+ start_path = str(Path(file_path).parent) if file_path else "."
188
+ config = load_config(start_path=start_path)
189
+
190
+ # --- evaluate ---
191
+ rules = build_default_rules(config=config)
192
+ result = evaluate_source(
193
+ source,
194
+ file_path=file_path,
195
+ language=language,
196
+ rules=rules,
197
+ mode=args.mode,
198
+ )
199
+
200
+ # --- local blocked-event logging ---
201
+ write_block_log(result)
202
+
203
+ # --- output ---
204
+ if args.json:
205
+ payload = _to_jsonable(result)
206
+ json.dump(payload, sys.stdout, indent=2)
207
+ sys.stdout.write("\n")
208
+ else:
209
+ print(format_human_report(result))
210
+
211
+ # exit code
212
+ if result.decision == Decision.BLOCK:
213
+ return 1
214
+ return 0
@@ -0,0 +1,43 @@
1
+ import os
2
+ import yaml
3
+ from typing import Any, Dict, Optional
4
+
5
+ CONFIG_FILENAME = ".watchllm.yaml"
6
+
7
+ def find_config(start_path: str = ".") -> Optional[str]:
8
+ """Search for .watchllm.yaml starting from start_path and moving upwards."""
9
+ current_path = os.path.abspath(start_path)
10
+
11
+ while True:
12
+ potential_config = os.path.join(current_path, CONFIG_FILENAME)
13
+ if os.path.isfile(potential_config):
14
+ return potential_config
15
+
16
+ parent = os.path.dirname(current_path)
17
+ if parent == current_path:
18
+ break
19
+ current_path = parent
20
+
21
+ return None
22
+
23
+ def load_config(config_path: Optional[str] = None, start_path: str = ".") -> Dict[str, Any]:
24
+ """Load the WatchLLM configuration from the given path or auto-discover it."""
25
+ if not config_path:
26
+ config_path = find_config(start_path)
27
+
28
+ if not config_path or not os.path.isfile(config_path):
29
+ return {}
30
+
31
+ try:
32
+ with open(config_path, "r", encoding="utf-8") as f:
33
+ config = yaml.safe_load(f)
34
+ return config if config else {}
35
+ except Exception as e:
36
+ print(f"Warning: Failed to load config from {config_path}: {e}")
37
+ return {}
38
+
39
+ def get_rule_config(config: Dict[str, Any], rule_name: str) -> Dict[str, Any]:
40
+ """Extract configuration for a specific rule."""
41
+ if not config or "rules" not in config or not config["rules"]:
42
+ return {}
43
+ return config["rules"].get(rule_name, {})