cluxion-agentplugin-supercoder 0.2.7__tar.gz → 0.2.8__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.
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/PKG-INFO +1 -1
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/__init__.py +1 -1
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/pyproject.toml +1 -1
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/scripts/repack_native_wheel.py +1 -5
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/cli.py +1 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/cursor.py +6 -2
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/hash_patch.py +4 -4
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/doctor/framework.py +13 -10
- cluxion_agentplugin_supercoder-0.2.8/src/cluxion_agentplugin_supercoder/doctor/probes.py +355 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/runner.py +12 -1
- cluxion_agentplugin_supercoder-0.2.8/tests/test_cursor.py +72 -0
- cluxion_agentplugin_supercoder-0.2.8/tests/test_doctor.py +246 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_hash_patch.py +3 -6
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/uv.lock +1 -1
- cluxion_agentplugin_supercoder-0.2.7/src/cluxion_agentplugin_supercoder/doctor/probes.py +0 -187
- cluxion_agentplugin_supercoder-0.2.7/tests/test_cursor.py +0 -14
- cluxion_agentplugin_supercoder-0.2.7/tests/test_doctor.py +0 -99
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/.github/workflows/ci.yml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/.github/workflows/publish.yml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/.gitignore +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/ARCHITECTURE.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/agent-surfaces.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/architecture.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/capabilities.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/design.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/installation.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/rust-architecture.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/Docs/tools.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/LICENSE +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/adapters/claude/.claude-plugin/plugin.json +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/adapters/claude/skills/supercoder/SKILL.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/adapters/codex/config-snippet.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/adapters/hermes/README.md +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/plugin.yaml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/Cargo.lock +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/Cargo.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/pyproject.toml +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/src/lib.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/src/main.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/src/outline.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/rust/supercoder_index/src/syntax.rs +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/__init__.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/line_budget.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/lint_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/queue.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/repo_map.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/retry_loop.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/safety.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/syntax_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/core/test_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/doctor/__init__.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/doctor/catalog.json +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/plugin.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/rust_bridge.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/src/cluxion_agentplugin_supercoder/schemas.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_line_budget.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_lint_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_plugin.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_queue.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_repo_map.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_retry_loop.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_rust_bridge.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_safety.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_syntax_gate.py +0 -0
- {cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/tests/test_test_gate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cluxion-agentplugin-supercoder
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-supercoder
|
|
6
6
|
Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-supercoder
|
{cluxion_agentplugin_supercoder-0.2.7 → cluxion_agentplugin_supercoder-0.2.8}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cluxion-agentplugin-supercoder"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
description = "Universal agent coding harness plugin: cursor logic, safe patch, line budget, Rust index, test gates."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -44,11 +44,7 @@ def _dist_info(tree: Path) -> Path:
|
|
|
44
44
|
|
|
45
45
|
def _native_tags(native_tree: Path) -> list[str]:
|
|
46
46
|
wheel_meta = (_dist_info(native_tree) / "WHEEL").read_text(encoding="utf-8")
|
|
47
|
-
tags = [
|
|
48
|
-
line.split(":", 1)[1].strip()
|
|
49
|
-
for line in wheel_meta.splitlines()
|
|
50
|
-
if line.startswith("Tag:")
|
|
51
|
-
]
|
|
47
|
+
tags = [line.split(":", 1)[1].strip() for line in wheel_meta.splitlines() if line.startswith("Tag:")]
|
|
52
48
|
if not tags:
|
|
53
49
|
raise SystemExit("native wheel has no Tag entries")
|
|
54
50
|
return tags
|
|
@@ -52,6 +52,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
52
52
|
def load_catalog_for_text(catalog_path):
|
|
53
53
|
# helper to avoid circular, but since framework has load_catalog
|
|
54
54
|
from cluxion_agentplugin_supercoder.doctor.framework import load_catalog
|
|
55
|
+
|
|
55
56
|
return load_catalog(Path(str(catalog_path)))
|
|
56
57
|
|
|
57
58
|
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from cluxion_agentplugin_supercoder.core.hash_patch import file_hash
|
|
9
|
+
from cluxion_agentplugin_supercoder.core.safety import pre_tool_gate
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass(frozen=True)
|
|
@@ -27,11 +28,14 @@ def read_window(
|
|
|
27
28
|
max_lines: int = 120,
|
|
28
29
|
purpose: str = "read",
|
|
29
30
|
) -> LineWindow:
|
|
31
|
+
gate = pre_tool_gate("read_window", {"path": rel_path}, workspace=root)
|
|
32
|
+
if gate.decision == "block":
|
|
33
|
+
raise PermissionError(gate.reason)
|
|
30
34
|
path = (root / rel_path).resolve()
|
|
31
35
|
if not path.exists():
|
|
32
36
|
raise FileNotFoundError(rel_path)
|
|
33
|
-
if not
|
|
34
|
-
raise PermissionError("
|
|
37
|
+
if not path.is_relative_to(root.resolve()):
|
|
38
|
+
raise PermissionError("workspace escape blocked")
|
|
35
39
|
text = path.read_text(encoding="utf-8")
|
|
36
40
|
lines = text.splitlines()
|
|
37
41
|
start = max(1, start_line)
|
|
@@ -77,7 +77,9 @@ def apply_patch(
|
|
|
77
77
|
exact = _exact_spans(text, old_text)
|
|
78
78
|
if exact:
|
|
79
79
|
start, end = exact[0]
|
|
80
|
-
return _commit(
|
|
80
|
+
return _commit(
|
|
81
|
+
path, text, start, end, new_text, "exact", expected_file_hash or current_hash, current_hash, 1.0
|
|
82
|
+
)
|
|
81
83
|
fuzzy = _best_fuzzy_span(text, old_text)
|
|
82
84
|
if fuzzy and fuzzy[3] >= fuzzy_threshold and not fuzzy[4]:
|
|
83
85
|
return _commit(
|
|
@@ -172,9 +174,7 @@ def _best_fuzzy_span(text: str, reference: str) -> tuple[int, int, str, float, b
|
|
|
172
174
|
def _atomic_write(path: Path, content: str) -> None:
|
|
173
175
|
"""Atomic replace via temp in same dir + fsync to prevent corruption on crash."""
|
|
174
176
|
dir_ = path.parent
|
|
175
|
-
with tempfile.NamedTemporaryFile(
|
|
176
|
-
mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp"
|
|
177
|
-
) as tmp:
|
|
177
|
+
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=dir_, delete=False, suffix=".tmp") as tmp:
|
|
178
178
|
tmp.write(content)
|
|
179
179
|
tmp.flush()
|
|
180
180
|
os.fsync(tmp.fileno())
|
|
@@ -45,14 +45,23 @@ class DoctorResult:
|
|
|
45
45
|
version: str
|
|
46
46
|
checks: tuple[CheckResult, ...]
|
|
47
47
|
|
|
48
|
+
@property
|
|
49
|
+
def summary(self) -> str:
|
|
50
|
+
if any(c.status == "fail" for c in self.checks):
|
|
51
|
+
return "fail"
|
|
52
|
+
if any(c.severity == "critical" and c.status == "skip" for c in self.checks):
|
|
53
|
+
return "degraded"
|
|
54
|
+
return "ok"
|
|
55
|
+
|
|
48
56
|
@property
|
|
49
57
|
def ok(self) -> bool:
|
|
50
|
-
return
|
|
58
|
+
return self.summary == "ok"
|
|
51
59
|
|
|
52
60
|
def to_json_object(self) -> dict[str, Any]:
|
|
53
61
|
return {
|
|
54
62
|
"plugin": self.plugin,
|
|
55
63
|
"version": self.version,
|
|
64
|
+
"summary": self.summary,
|
|
56
65
|
"checks": [
|
|
57
66
|
{
|
|
58
67
|
"check_id": c.check_id,
|
|
@@ -68,9 +77,7 @@ class DoctorResult:
|
|
|
68
77
|
|
|
69
78
|
|
|
70
79
|
class DoctorContext:
|
|
71
|
-
def __init__(
|
|
72
|
-
self, cwd: Path, hermes_bin: str, run: Callable[[list[str]], subprocess.CompletedProcess]
|
|
73
|
-
) -> None:
|
|
80
|
+
def __init__(self, cwd: Path, hermes_bin: str, run: Callable[[list[str]], subprocess.CompletedProcess]) -> None:
|
|
74
81
|
self.cwd = cwd
|
|
75
82
|
self.hermes_bin = hermes_bin
|
|
76
83
|
self.run = run
|
|
@@ -151,14 +158,10 @@ def run_doctor(
|
|
|
151
158
|
|
|
152
159
|
|
|
153
160
|
def render_json(result: DoctorResult) -> str:
|
|
154
|
-
return json.dumps(
|
|
155
|
-
result.to_json_object(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
156
|
-
)
|
|
161
|
+
return json.dumps(result.to_json_object(), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
157
162
|
|
|
158
163
|
|
|
159
|
-
def render_text(
|
|
160
|
-
result: DoctorResult, catalog: tuple[CatalogEntry, ...], *, verbose: bool = False
|
|
161
|
-
) -> str:
|
|
164
|
+
def render_text(result: DoctorResult, catalog: tuple[CatalogEntry, ...], *, verbose: bool = False) -> str:
|
|
162
165
|
entry_map = {e.check_id: e for e in catalog}
|
|
163
166
|
lines: list[str] = []
|
|
164
167
|
for c in result.checks:
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Plugin-specific probes for supercoder doctor. Cross-cutting + selected specific checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .framework import DoctorContext
|
|
15
|
+
|
|
16
|
+
PROBES: dict[str, Callable[[DoctorContext], tuple[str, str]]] = {}
|
|
17
|
+
|
|
18
|
+
_HERMES_ABSENT_SKIP = "hermes binary not on PATH — cannot verify"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _hermes_path(ctx: DoctorContext) -> str | None:
|
|
22
|
+
return shutil.which(ctx.hermes_bin)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _register(name: str):
|
|
26
|
+
def deco(fn):
|
|
27
|
+
PROBES[name] = fn
|
|
28
|
+
return fn
|
|
29
|
+
|
|
30
|
+
return deco
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@_register("hermes_on_path")
|
|
34
|
+
def hermes_on_path(ctx: DoctorContext) -> tuple[str, str]:
|
|
35
|
+
p = _hermes_path(ctx)
|
|
36
|
+
if p:
|
|
37
|
+
return "pass", str(p)
|
|
38
|
+
return "skip", _HERMES_ABSENT_SKIP
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@_register("hermes_version")
|
|
42
|
+
def hermes_version(ctx: DoctorContext) -> tuple[str, str]:
|
|
43
|
+
if _hermes_path(ctx) is None:
|
|
44
|
+
return "skip", _HERMES_ABSENT_SKIP
|
|
45
|
+
try:
|
|
46
|
+
cp = ctx.run([ctx.hermes_bin, "--version"])
|
|
47
|
+
if cp.returncode == 0 and "Hermes Agent v" in cp.stdout:
|
|
48
|
+
return "pass", cp.stdout.strip()
|
|
49
|
+
return "fail", cp.stdout.strip() or cp.stderr.strip()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return "fail", f"run error: {e}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@_register("hermes_oneshot_flag")
|
|
55
|
+
def hermes_oneshot_flag(ctx: DoctorContext) -> tuple[str, str]:
|
|
56
|
+
if _hermes_path(ctx) is None:
|
|
57
|
+
return "skip", _HERMES_ABSENT_SKIP
|
|
58
|
+
try:
|
|
59
|
+
cp = ctx.run([ctx.hermes_bin, "--help"])
|
|
60
|
+
out = cp.stdout + cp.stderr
|
|
61
|
+
if "-z" in out and "--oneshot" in out:
|
|
62
|
+
return "pass", "present"
|
|
63
|
+
return "fail", "missing in --help"
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return "fail", f"run error: {e}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@_register("entry_point_registered")
|
|
69
|
+
def entry_point_registered(ctx: DoctorContext) -> tuple[str, str]:
|
|
70
|
+
try:
|
|
71
|
+
eps = importlib.metadata.entry_points(group="hermes_agent.plugins")
|
|
72
|
+
for ep in eps:
|
|
73
|
+
if "cluxion-agentplugin-supercoder" in (ep.name or "").lower() or "cluxion_agentplugin_supercoder" in (
|
|
74
|
+
ep.value or ""
|
|
75
|
+
):
|
|
76
|
+
mod = ep.load()
|
|
77
|
+
if hasattr(mod, "register") and callable(mod.register):
|
|
78
|
+
return "pass", ep.value or str(ep)
|
|
79
|
+
return "warn", "entry point metadata not present (dev PYTHONPATH ok)"
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return "fail", f"metadata error: {e}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@_register("toolset_valid")
|
|
85
|
+
def toolset_valid(ctx: DoctorContext) -> tuple[str, str]:
|
|
86
|
+
if _hermes_path(ctx) is None:
|
|
87
|
+
return "skip", _HERMES_ABSENT_SKIP
|
|
88
|
+
try:
|
|
89
|
+
cp = ctx.run([ctx.hermes_bin, "tools", "list"])
|
|
90
|
+
if cp.returncode == 0 and "supercoder" in cp.stdout:
|
|
91
|
+
return "pass", "supercoder present"
|
|
92
|
+
return "fail", "supercoder not in tools list"
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return "fail", f"run error: {e}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@_register("install_integrity")
|
|
98
|
+
def install_integrity(ctx: DoctorContext) -> tuple[str, str]:
|
|
99
|
+
try:
|
|
100
|
+
from cluxion_agentplugin_supercoder import __version__ as pkg_version
|
|
101
|
+
|
|
102
|
+
dist_version = importlib.metadata.version("cluxion-agentplugin-supercoder")
|
|
103
|
+
if dist_version == pkg_version:
|
|
104
|
+
return "pass", dist_version
|
|
105
|
+
return "warn", f"dist={dist_version} pkg={pkg_version}"
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return "warn", f"version error: {e}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@_register("native_module_importable")
|
|
111
|
+
def native_module_importable(ctx: DoctorContext) -> tuple[str, str]:
|
|
112
|
+
try:
|
|
113
|
+
mod = __import__("supercoder_index_native")
|
|
114
|
+
if hasattr(mod, "run"):
|
|
115
|
+
return "pass", "imported (native backend available)"
|
|
116
|
+
return "warn", "imported but expected symbols missing"
|
|
117
|
+
except Exception:
|
|
118
|
+
return "warn", "native missing → using fallback (slower)"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# plugin-specific probes (deterministic ones only) - for supercoder we can add if symbols found
|
|
122
|
+
# for now, handler_exception_coverage is cross-cutting
|
|
123
|
+
@_register("handler_exception_coverage")
|
|
124
|
+
def handler_exception_coverage(ctx: DoctorContext) -> tuple[str, str]:
|
|
125
|
+
try:
|
|
126
|
+
from cluxion_agentplugin_supercoder import runner
|
|
127
|
+
from cluxion_agentplugin_supercoder.plugin import _wrap
|
|
128
|
+
|
|
129
|
+
def bad_cb(_payload: dict[str, object]) -> runner.ToolResult:
|
|
130
|
+
raise TypeError("test TypeError for coverage")
|
|
131
|
+
|
|
132
|
+
result = _wrap(bad_cb)({})
|
|
133
|
+
parsed = json.loads(result)
|
|
134
|
+
if parsed.get("ok") is False and "TypeError" in str(parsed.get("error", "")):
|
|
135
|
+
return "pass", "degraded to error JSON"
|
|
136
|
+
return "fail", f"no error json: {result[:100]}"
|
|
137
|
+
except Exception as e:
|
|
138
|
+
return "skip", f"cannot invoke guard: {e}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# NEW deterministic probes for previously-skipped catalog checks (import-avail, json-det, abi3/sqlite patterns adapted)
|
|
142
|
+
# hermes_requirements_installed (import availability using find_spec to satisfy linter)
|
|
143
|
+
@_register("hermes_requirements_installed")
|
|
144
|
+
def hermes_requirements_installed(ctx: DoctorContext) -> tuple[str, str]:
|
|
145
|
+
try:
|
|
146
|
+
if importlib.util.find_spec("psutil") and importlib.util.find_spec("yaml"):
|
|
147
|
+
return "pass", "psutil+PyYAML importable"
|
|
148
|
+
return "warn", "missing dep"
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return "skip", f"import check error: {e}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# repo_map_deterministic (json determinism + real call)
|
|
154
|
+
@_register("repo_map_deterministic")
|
|
155
|
+
def repo_map_deterministic(ctx: DoctorContext) -> tuple[str, str]:
|
|
156
|
+
try:
|
|
157
|
+
from cluxion_agentplugin_supercoder.core.repo_map import build_repo_map
|
|
158
|
+
|
|
159
|
+
m1 = build_repo_map(ctx.cwd, budget_chars=2000)
|
|
160
|
+
m2 = build_repo_map(ctx.cwd, budget_chars=2000)
|
|
161
|
+
|
|
162
|
+
def strip(d):
|
|
163
|
+
if isinstance(d, dict):
|
|
164
|
+
return {k: strip(v) for k, v in d.items() if k != "_stats"}
|
|
165
|
+
if isinstance(d, list):
|
|
166
|
+
return [strip(x) for x in d]
|
|
167
|
+
return d
|
|
168
|
+
|
|
169
|
+
if strip(m1) == strip(m2):
|
|
170
|
+
j1 = json.dumps(m1, sort_keys=True)
|
|
171
|
+
j2 = json.dumps(m2, sort_keys=True)
|
|
172
|
+
if j1 == j2:
|
|
173
|
+
return "pass", "deterministic + json roundtrip ok"
|
|
174
|
+
return "warn", "map match but json not"
|
|
175
|
+
return "fail", "non-deterministic"
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return "skip", f"cannot run: {e}"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ruff_binary_discoverable (real env/path probe)
|
|
181
|
+
@_register("ruff_binary_discoverable")
|
|
182
|
+
def ruff_binary_discoverable(ctx: DoctorContext) -> tuple[str, str]:
|
|
183
|
+
try:
|
|
184
|
+
envb = os.environ.get("CLUXION_SUPERCODER_RUFF_BIN")
|
|
185
|
+
if envb and Path(envb).is_file():
|
|
186
|
+
return "pass", envb
|
|
187
|
+
cands = [Path(ctx.cwd) / ".venv/bin/ruff", shutil.which("ruff")]
|
|
188
|
+
for c in cands:
|
|
189
|
+
if c and Path(c).is_file():
|
|
190
|
+
return "pass", str(c)
|
|
191
|
+
return "warn", "no ruff binary (advisory)"
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return "skip", f"probe error: {e}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# file_hash_consistency (real check)
|
|
197
|
+
@_register("file_hash_consistency")
|
|
198
|
+
def file_hash_consistency(ctx: DoctorContext) -> tuple[str, str]:
|
|
199
|
+
try:
|
|
200
|
+
from cluxion_agentplugin_supercoder.core.hash_patch import _normalize_newlines, file_hash
|
|
201
|
+
|
|
202
|
+
c = "a=1\r\nb=2"
|
|
203
|
+
if file_hash(c) == file_hash(_normalize_newlines(c)):
|
|
204
|
+
return "pass", "CRLF safe"
|
|
205
|
+
return "fail", "hash mismatch"
|
|
206
|
+
except Exception as e:
|
|
207
|
+
return "skip", f"hash error: {e}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
_SECRET_BLOCKED = "secret file access blocked"
|
|
211
|
+
_ESCAPE_BLOCKED = "workspace escape blocked"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _assert_tool_blocks(
|
|
215
|
+
tool_fn: Callable[[dict[str, object]], object],
|
|
216
|
+
*,
|
|
217
|
+
cwd: str,
|
|
218
|
+
path: str,
|
|
219
|
+
expected_error: str,
|
|
220
|
+
extra: dict[str, object] | None = None,
|
|
221
|
+
) -> str | None:
|
|
222
|
+
from cluxion_agentplugin_supercoder import runner
|
|
223
|
+
|
|
224
|
+
payload: dict[str, object] = {"cwd": cwd, "path": path}
|
|
225
|
+
if extra:
|
|
226
|
+
payload.update(extra)
|
|
227
|
+
result = tool_fn(payload)
|
|
228
|
+
if not isinstance(result, runner.ToolResult):
|
|
229
|
+
return f"unexpected result type for {path}"
|
|
230
|
+
if result.ok or result.payload.get("error") != expected_error:
|
|
231
|
+
return f"{tool_fn.__name__} on {path}: ok={result.ok} error={result.payload.get('error')}"
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@_register("path_security_secrets_blocked")
|
|
236
|
+
def path_security_secrets_blocked(ctx: DoctorContext) -> tuple[str, str]:
|
|
237
|
+
try:
|
|
238
|
+
from cluxion_agentplugin_supercoder import runner
|
|
239
|
+
|
|
240
|
+
with tempfile.TemporaryDirectory() as td:
|
|
241
|
+
root = Path(td)
|
|
242
|
+
(root / ".env").write_text("KEY=secret", encoding="utf-8")
|
|
243
|
+
cred = root / "config" / "credentials"
|
|
244
|
+
cred.mkdir(parents=True)
|
|
245
|
+
(cred / "db.json").write_text("{}", encoding="utf-8")
|
|
246
|
+
for rel in (".env", "config/credentials/db.json"):
|
|
247
|
+
for tool_fn, extra in (
|
|
248
|
+
(runner.read_window_tool, None),
|
|
249
|
+
(runner.patch_tool, {"old_text": "x", "new_text": "y", "syntax_gate": False}),
|
|
250
|
+
):
|
|
251
|
+
err = _assert_tool_blocks(
|
|
252
|
+
tool_fn,
|
|
253
|
+
cwd=str(root),
|
|
254
|
+
path=rel,
|
|
255
|
+
expected_error=_SECRET_BLOCKED,
|
|
256
|
+
extra=extra,
|
|
257
|
+
)
|
|
258
|
+
if err:
|
|
259
|
+
return "fail", err
|
|
260
|
+
return "pass", "read_window_tool + patch_tool block .env and credentials"
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return "skip", f"cannot run: {e}"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@_register("hermes_context_workspace_root")
|
|
266
|
+
def hermes_context_workspace_root(ctx: DoctorContext) -> tuple[str, str]:
|
|
267
|
+
try:
|
|
268
|
+
from cluxion_agentplugin_supercoder import runner
|
|
269
|
+
|
|
270
|
+
with tempfile.TemporaryDirectory() as td:
|
|
271
|
+
base = Path(td)
|
|
272
|
+
outside = base / "outside"
|
|
273
|
+
outside.mkdir()
|
|
274
|
+
(outside / "secret.txt").write_text("leaked", encoding="utf-8")
|
|
275
|
+
workspace = base / "work"
|
|
276
|
+
workspace.mkdir()
|
|
277
|
+
sibling = base / "work2"
|
|
278
|
+
sibling.mkdir()
|
|
279
|
+
(sibling / "x").write_text("leaked", encoding="utf-8")
|
|
280
|
+
for path in ("../outside/secret.txt", "../work2/x"):
|
|
281
|
+
err = _assert_tool_blocks(
|
|
282
|
+
runner.read_window_tool,
|
|
283
|
+
cwd=str(workspace),
|
|
284
|
+
path=path,
|
|
285
|
+
expected_error=_ESCAPE_BLOCKED,
|
|
286
|
+
)
|
|
287
|
+
if err:
|
|
288
|
+
return "fail", err
|
|
289
|
+
return "pass", "traversal + sibling-prefix escape blocked"
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return "skip", f"cannot run: {e}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@_register("patch_cursor_validity")
|
|
295
|
+
def patch_cursor_validity(ctx: DoctorContext) -> tuple[str, str]:
|
|
296
|
+
try:
|
|
297
|
+
from cluxion_agentplugin_supercoder import runner
|
|
298
|
+
from cluxion_agentplugin_supercoder.core.cursor import read_window
|
|
299
|
+
|
|
300
|
+
with tempfile.TemporaryDirectory() as td:
|
|
301
|
+
root = Path(td)
|
|
302
|
+
target = root / "t.py"
|
|
303
|
+
target.write_text("x=1", encoding="utf-8")
|
|
304
|
+
window = read_window(root, "t.py")
|
|
305
|
+
target.write_text("x=2", encoding="utf-8")
|
|
306
|
+
result = runner.patch_tool(
|
|
307
|
+
{
|
|
308
|
+
"cwd": str(root),
|
|
309
|
+
"path": "t.py",
|
|
310
|
+
"old_text": "x=1",
|
|
311
|
+
"new_text": "x=3",
|
|
312
|
+
"expected_file_hash": window.file_hash,
|
|
313
|
+
"syntax_gate": False,
|
|
314
|
+
"lint_gate": False,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
if result.ok:
|
|
318
|
+
return "fail", "patch applied with stale hash"
|
|
319
|
+
if result.payload.get("strategy") != "stale_file":
|
|
320
|
+
return "fail", f"expected stale_file, got {result.payload.get('strategy')}"
|
|
321
|
+
return "pass", "stale hash blocked"
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return "skip", f"cannot run: {e}"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@_register("stale_cursor_protection_enforced")
|
|
327
|
+
def stale_cursor_protection_enforced(ctx: DoctorContext) -> tuple[str, str]:
|
|
328
|
+
try:
|
|
329
|
+
from cluxion_agentplugin_supercoder import runner
|
|
330
|
+
|
|
331
|
+
with tempfile.TemporaryDirectory() as td:
|
|
332
|
+
root = Path(td)
|
|
333
|
+
(root / "t.py").write_text("x=1", encoding="utf-8")
|
|
334
|
+
result = runner.patch_tool(
|
|
335
|
+
{
|
|
336
|
+
"cwd": str(root),
|
|
337
|
+
"path": "t.py",
|
|
338
|
+
"old_text": "x",
|
|
339
|
+
"new_text": "y",
|
|
340
|
+
"stale_cursor": True,
|
|
341
|
+
"syntax_gate": False,
|
|
342
|
+
"lint_gate": False,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
if result.ok:
|
|
346
|
+
return "fail", "patch applied with stale_cursor=True"
|
|
347
|
+
error = str(result.payload.get("error", ""))
|
|
348
|
+
if "stale cursor" not in error:
|
|
349
|
+
return "fail", f"unexpected error: {error}"
|
|
350
|
+
return "pass", "stale_cursor flag blocked"
|
|
351
|
+
except Exception as e:
|
|
352
|
+
return "skip", f"cannot run: {e}"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# note: other checks in catalog will be reported as skip (no probe)
|
|
@@ -73,6 +73,14 @@ def plan(payload: Mapping[str, object]) -> ToolResult:
|
|
|
73
73
|
def read_window_tool(payload: Mapping[str, object]) -> ToolResult:
|
|
74
74
|
root = _workspace(payload)
|
|
75
75
|
rel = str(payload.get("path", "")).strip()
|
|
76
|
+
gate = pre_tool_gate(
|
|
77
|
+
"read_window",
|
|
78
|
+
payload,
|
|
79
|
+
workspace=root,
|
|
80
|
+
stale_cursor=bool(payload.get("stale_cursor", False)),
|
|
81
|
+
)
|
|
82
|
+
if gate.decision == "block":
|
|
83
|
+
return ToolResult(False, {"error": gate.reason})
|
|
76
84
|
start = _int(payload.get("start_line", 1), 1)
|
|
77
85
|
max_lines = _int(payload.get("max_lines", 120), 120)
|
|
78
86
|
decision = budget_for("inspect", requested_lines=max_lines)
|
|
@@ -182,7 +190,10 @@ def repo_map_tool(payload: Mapping[str, object]) -> ToolResult:
|
|
|
182
190
|
result = repo_map.build_repo_map(
|
|
183
191
|
_workspace(payload),
|
|
184
192
|
max_files=_int(payload.get("max_files", repo_map.DEFAULT_MAX_FILES), repo_map.DEFAULT_MAX_FILES),
|
|
185
|
-
max_symbols_per_file=_int(
|
|
193
|
+
max_symbols_per_file=_int(
|
|
194
|
+
payload.get("max_symbols_per_file", repo_map.DEFAULT_MAX_SYMBOLS_PER_FILE),
|
|
195
|
+
repo_map.DEFAULT_MAX_SYMBOLS_PER_FILE,
|
|
196
|
+
),
|
|
186
197
|
budget_chars=_int(payload.get("budget_chars", repo_map.DEFAULT_BUDGET_CHARS), repo_map.DEFAULT_BUDGET_CHARS),
|
|
187
198
|
)
|
|
188
199
|
return ToolResult(bool(result.get("ok", False)), {key: value for key, value in result.items() if key != "ok"})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from cluxion_agentplugin_supercoder import runner
|
|
8
|
+
from cluxion_agentplugin_supercoder.core.cursor import read_window
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_read_window_bounds(tmp_path: Path) -> None:
|
|
12
|
+
path = tmp_path / "sample.py"
|
|
13
|
+
path.write_text("\n".join(f"line{i}" for i in range(1, 201)), encoding="utf-8")
|
|
14
|
+
window = read_window(tmp_path, "sample.py", start_line=10, max_lines=5)
|
|
15
|
+
assert window.start_line == 10
|
|
16
|
+
assert window.end_line == 14
|
|
17
|
+
assert "line10" in window.content
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize("rel", [".env", "config/credentials/db.json"])
|
|
21
|
+
def test_read_window_blocks_secret_files(tmp_path: Path, rel: str) -> None:
|
|
22
|
+
target = tmp_path / rel
|
|
23
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
target.write_text("AWS_SECRET=leaked", encoding="utf-8")
|
|
25
|
+
with pytest.raises(PermissionError, match="secret file access blocked"):
|
|
26
|
+
read_window(tmp_path, rel)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_read_window_blocks_sibling_directory_prefix_escape(tmp_path: Path) -> None:
|
|
30
|
+
workspace = tmp_path / "work"
|
|
31
|
+
workspace.mkdir()
|
|
32
|
+
sibling = tmp_path / "work2"
|
|
33
|
+
sibling.mkdir()
|
|
34
|
+
(sibling / "secret.py").write_text("leaked", encoding="utf-8")
|
|
35
|
+
with pytest.raises(PermissionError, match="workspace escape blocked"):
|
|
36
|
+
read_window(workspace, "../work2/secret.py")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_read_window_blocks_plain_traversal(tmp_path: Path) -> None:
|
|
40
|
+
workspace = tmp_path / "work"
|
|
41
|
+
workspace.mkdir()
|
|
42
|
+
with pytest.raises(PermissionError, match="workspace escape blocked"):
|
|
43
|
+
read_window(workspace, "../../etc/passwd")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize("rel", [".env", "config/credentials/db.json"])
|
|
47
|
+
def test_read_window_tool_blocks_secret_files(tmp_path: Path, rel: str) -> None:
|
|
48
|
+
target = tmp_path / rel
|
|
49
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
target.write_text("AWS_SECRET=leaked", encoding="utf-8")
|
|
51
|
+
result = runner.read_window_tool({"cwd": str(tmp_path), "path": rel})
|
|
52
|
+
assert result.ok is False
|
|
53
|
+
assert result.payload["error"] == "secret file access blocked"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_read_window_tool_blocks_sibling_directory_prefix_escape(tmp_path: Path) -> None:
|
|
57
|
+
workspace = tmp_path / "work"
|
|
58
|
+
workspace.mkdir()
|
|
59
|
+
sibling = tmp_path / "work2"
|
|
60
|
+
sibling.mkdir()
|
|
61
|
+
(sibling / "secret.py").write_text("leaked", encoding="utf-8")
|
|
62
|
+
result = runner.read_window_tool({"cwd": str(workspace), "path": "../work2/secret.py"})
|
|
63
|
+
assert result.ok is False
|
|
64
|
+
assert result.payload["error"] == "workspace escape blocked"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_read_window_tool_allows_normal_in_workspace_file(tmp_path: Path) -> None:
|
|
68
|
+
path = tmp_path / "sample.py"
|
|
69
|
+
path.write_text("print('ok')\n", encoding="utf-8")
|
|
70
|
+
result = runner.read_window_tool({"cwd": str(tmp_path), "path": "sample.py"})
|
|
71
|
+
assert result.ok is True
|
|
72
|
+
assert "print('ok')" in str(result.payload["content"])
|