arkaos 2.43.0 → 2.44.0

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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.43.0
1
+ 2.44.0
@@ -0,0 +1,155 @@
1
+ """Client-name leak scanner (PR22 v2.44.0).
2
+
3
+ Reads the user-local ``~/.arkaos/redaction-clients.json`` and scans
4
+ source files / arbitrary text for word-boundary matches. Wired into
5
+ the release preflight gate as a blocking check.
6
+
7
+ Closes a real process gap: PR20 v2.42.0 shipped 9 client names in a
8
+ production constant; the Quality Gate did not flag it; operator
9
+ caught it manually pre-commit. Automation > checklist.
10
+
11
+ Empty / missing config is a no-op by design — there are no false
12
+ positives in CI clones that lack the operator's local list.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Iterable
22
+
23
+ _DEFAULT_CONFIG_PATH = Path.home() / ".arkaos" / "redaction-clients.json"
24
+ _MAX_FILE_BYTES = 10 * 1024 * 1024 # 10 MB per-file cap
25
+ _SCAN_EXTENSIONS: frozenset[str] = frozenset({
26
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs",
27
+ ".json", ".yaml", ".yml", ".toml", ".md", ".sh", ".txt",
28
+ })
29
+ _EXCLUDE_DIRS: frozenset[str] = frozenset({
30
+ "node_modules", "__pycache__", ".venv", ".git", "dist", "build",
31
+ })
32
+ _LINE_EXCERPT_LIMIT = 200
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class LeakHit:
37
+ """One client-name occurrence in a scanned file."""
38
+ path: Path
39
+ line_number: int
40
+ line_excerpt: str
41
+ matched_token: str
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ScanReport:
46
+ """Result of a scan."""
47
+ config_path: Path
48
+ pattern_count: int
49
+ files_scanned: int
50
+ hits: list[LeakHit]
51
+
52
+ @property
53
+ def clean(self) -> bool:
54
+ return not self.hits
55
+
56
+
57
+ def scan_paths(
58
+ paths: Iterable[Path],
59
+ *,
60
+ config_path: Path | None = None,
61
+ ) -> ScanReport:
62
+ """Recursively scan the given paths for client-name leaks."""
63
+ cfg = config_path or _DEFAULT_CONFIG_PATH
64
+ patterns = _load_patterns(cfg)
65
+ if not patterns:
66
+ return ScanReport(config_path=cfg, pattern_count=0, files_scanned=0, hits=[])
67
+ regex = _build_regex(patterns)
68
+ hits: list[LeakHit] = []
69
+ files_scanned = 0
70
+ for path in paths:
71
+ for file_path in _iter_scannable_files(Path(path)):
72
+ files_scanned += 1
73
+ hits.extend(_scan_file(file_path, regex))
74
+ return ScanReport(
75
+ config_path=cfg,
76
+ pattern_count=len(patterns),
77
+ files_scanned=files_scanned,
78
+ hits=hits,
79
+ )
80
+
81
+
82
+ def scan_text(
83
+ text: str,
84
+ *,
85
+ config_path: Path | None = None,
86
+ ) -> list[str]:
87
+ """Return the list of matched tokens (lowercased) found in *text*."""
88
+ cfg = config_path or _DEFAULT_CONFIG_PATH
89
+ patterns = _load_patterns(cfg)
90
+ if not patterns:
91
+ return []
92
+ regex = _build_regex(patterns)
93
+ return [m.group(1).lower() for m in regex.finditer(text)]
94
+
95
+
96
+ def _load_patterns(config_path: Path) -> tuple[str, ...]:
97
+ try:
98
+ data = json.loads(config_path.read_text(encoding="utf-8"))
99
+ raw = data.get("clients", [])
100
+ return tuple(
101
+ str(c).strip().lower() for c in raw
102
+ if c and isinstance(c, str)
103
+ )
104
+ except (OSError, json.JSONDecodeError, AttributeError):
105
+ return ()
106
+
107
+
108
+ def _build_regex(patterns: tuple[str, ...]) -> re.Pattern[str]:
109
+ escaped = [re.escape(p) for p in patterns]
110
+ return re.compile(
111
+ r"(?<![a-z0-9])(" + "|".join(escaped) + r")(?![a-z0-9])",
112
+ re.IGNORECASE,
113
+ )
114
+
115
+
116
+ def _iter_scannable_files(root: Path):
117
+ if root.is_file():
118
+ if _is_scannable(root):
119
+ yield root
120
+ return
121
+ if not root.is_dir():
122
+ return
123
+ for entry in sorted(root.rglob("*")):
124
+ if any(part in _EXCLUDE_DIRS for part in entry.parts):
125
+ continue
126
+ if entry.is_file() and _is_scannable(entry):
127
+ yield entry
128
+
129
+
130
+ def _is_scannable(path: Path) -> bool:
131
+ if path.suffix.lower() not in _SCAN_EXTENSIONS:
132
+ return False
133
+ try:
134
+ if path.stat().st_size > _MAX_FILE_BYTES:
135
+ return False
136
+ except OSError:
137
+ return False
138
+ return True
139
+
140
+
141
+ def _scan_file(path: Path, regex: re.Pattern[str]) -> list[LeakHit]:
142
+ try:
143
+ text = path.read_text(encoding="utf-8")
144
+ except (OSError, UnicodeDecodeError):
145
+ return []
146
+ hits: list[LeakHit] = []
147
+ for line_no, line in enumerate(text.splitlines(), start=1):
148
+ for match in regex.finditer(line):
149
+ hits.append(LeakHit(
150
+ path=path,
151
+ line_number=line_no,
152
+ line_excerpt=line[:_LINE_EXCERPT_LIMIT],
153
+ matched_token=match.group(1).lower(),
154
+ ))
155
+ return hits
@@ -61,10 +61,69 @@ def run_preflight(
61
61
  check_gh_auth(),
62
62
  check_git_remote(),
63
63
  check_git_clean(),
64
+ check_no_client_name_leaks(repo_root=root),
64
65
  ]
65
66
  return _aggregate(results)
66
67
 
67
68
 
69
+ def check_no_client_name_leaks(
70
+ *, repo_root: Path | None = None,
71
+ ) -> CheckResult:
72
+ """Scan tracked source files for client identifiers (PR22 v2.44.0).
73
+
74
+ No-op when the operator's ~/.arkaos/redaction-clients.json is absent
75
+ or empty — no false positives in CI clones lacking the local config.
76
+ """
77
+ from core.governance.leak_scanner import scan_paths
78
+
79
+ root = repo_root or _DEFAULT_REPO_ROOT
80
+ paths = _collect_tracked_paths(root)
81
+ if paths is None:
82
+ return CheckResult(
83
+ name="no-client-name-leaks", passed=True,
84
+ reason="git ls-files unavailable — skipped",
85
+ )
86
+ report = scan_paths(paths)
87
+ return _format_leak_check_result(report)
88
+
89
+
90
+ def _collect_tracked_paths(root: Path) -> list[Path] | None:
91
+ tracked = _run(["git", "-C", str(root), "ls-files", "--cached"])
92
+ if tracked is None or tracked.returncode != 0:
93
+ return None
94
+ return [root / line for line in tracked.stdout.splitlines() if line.strip()]
95
+
96
+
97
+ def _format_leak_check_result(report) -> CheckResult:
98
+ if report.pattern_count == 0:
99
+ return CheckResult(
100
+ name="no-client-name-leaks", passed=True,
101
+ reason="no redaction config — scan skipped (no-op)",
102
+ )
103
+ if report.clean:
104
+ return CheckResult(
105
+ name="no-client-name-leaks", passed=True,
106
+ reason=f"{report.files_scanned} file(s) scanned, no leaks",
107
+ )
108
+ first = report.hits[0]
109
+ # PR22 v2.44.0 introduces this check at WARNING severity to ship the
110
+ # scanner without blocking on pre-existing leaks in test fixtures
111
+ # (test_dreaming.py and siblings) + historical CHANGELOG/ADR
112
+ # references. PR23 cleans those up and flips severity to "blocking".
113
+ return CheckResult(
114
+ name="no-client-name-leaks", passed=False, severity="warning",
115
+ reason=(
116
+ f"{len(report.hits)} leak(s) — first: "
117
+ f"{first.path.name}:{first.line_number} matched `{first.matched_token}`"
118
+ ),
119
+ remediation=(
120
+ "move the literal to ~/.arkaos/ or sanitize before commit; "
121
+ "PR22 scope intentionally warning-only — PR23 will scrub "
122
+ "historical leaks and flip this check to blocking"
123
+ ),
124
+ )
125
+
126
+
68
127
  def _load_repo_versions(
69
128
  root: Path,
70
129
  ) -> tuple[str | None, str | None, str | None] | None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.43.0",
3
+ "version": "2.44.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.43.0"
3
+ version = "2.44.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}