Skip to content

Commit dda04cc

Browse files
committed
fix: reject non-synth skill writes
1 parent 3043bf9 commit dda04cc

3 files changed

Lines changed: 50 additions & 3 deletions

File tree

‎src/codex_self_evolution/skill_synthesis/inventory.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ def snapshot_synth_skills(skills_root: Path) -> dict[str, dict[str, Any]]:
9191
return snapshot
9292

9393

94+
def snapshot_skill_docs(skills_root: Path) -> dict[str, dict[str, Any]]:
95+
snapshot: dict[str, dict[str, Any]] = {}
96+
if not skills_root.is_dir():
97+
return snapshot
98+
for child in sorted(skills_root.iterdir()):
99+
if not child.is_dir():
100+
continue
101+
meta = read_skill_metadata(child)
102+
if meta is not None:
103+
snapshot[str(child / "SKILL.md")] = meta
104+
return snapshot
105+
106+
94107
def changed_paths(before: dict[str, dict[str, Any]], after: dict[str, dict[str, Any]]) -> list[str]:
95108
paths = set(before) | set(after)
96109
return sorted(

‎src/codex_self_evolution/skill_synthesis/runner.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
materialize_evidence_workspace,
1818
update_evidence_index,
1919
)
20-
from .inventory import changed_paths, read_skills_inventory, snapshot_synth_skills, write_published_index
20+
from .inventory import changed_paths, read_skills_inventory, snapshot_skill_docs, snapshot_synth_skills, write_published_index
2121
from .paths import build_skill_synthesis_paths, resolve_real_skills_root
2222
from .validation import clear_invalid_marker, mark_invalid, validate_synth_skill
2323

@@ -106,8 +106,10 @@ def _run_locked(
106106
evidence_index=evidence_index,
107107
)
108108
real_before = snapshot_synth_skills(real_skills_root)
109+
real_all_before = snapshot_skill_docs(real_skills_root)
109110
skills_root = Path(tempfile.mkdtemp(prefix=f"csep-skill-synthesis-{run_id}-")) / "skills" if dry_run else real_skills_root
110111
before = snapshot_synth_skills(skills_root)
112+
all_before = snapshot_skill_docs(skills_root)
111113
inventory = read_skills_inventory(real_skills_root)
112114
materialize_evidence_workspace(
113115
paths,
@@ -141,15 +143,19 @@ def _run_locked(
141143

142144
after = snapshot_synth_skills(skills_root)
143145
real_after = snapshot_synth_skills(real_skills_root)
146+
all_after = snapshot_skill_docs(skills_root)
147+
real_all_after = snapshot_skill_docs(real_skills_root)
144148
detected = changed_paths(before, after)
149+
namespace_violations = _non_synth_changed_paths(changed_paths(all_before, all_after))
145150
changed_real = changed_paths(real_before, real_after) if dry_run else []
151+
changed_real_all = changed_paths(real_all_before, real_all_after) if dry_run else []
146152
valid, invalid, retired = _validate_changed(run_id, detected)
147153
result_missing = bool(agent_result.get("result_missing"))
148154
result_invalid = bool(agent_result.get("result_invalid"))
149155
mismatch = _reported_written(agent_result) != detected
150156
dry_run_leak = dry_run and bool(changed_real)
151157
status = "success"
152-
if error or dry_run_leak:
158+
if error or dry_run_leak or namespace_violations:
153159
status = "error"
154160
elif invalid or mismatch or result_missing or result_invalid:
155161
status = "partial"
@@ -166,7 +172,8 @@ def _run_locked(
166172
"seen_before_count": sum(1 for item in evidence if item.get("seen_before")),
167173
"reported_written": _reported_written(agent_result),
168174
"detected_changed": detected,
169-
"changed_real_paths": changed_real,
175+
"changed_real_paths": changed_real_all or changed_real,
176+
"namespace_violations": namespace_violations,
170177
"valid": valid,
171178
"invalid": invalid,
172179
"retired": retired,
@@ -193,6 +200,13 @@ def _reported_written(agent_result: dict[str, Any]) -> list[str]:
193200
return sorted(out)
194201

195202

203+
def _non_synth_changed_paths(paths: list[str]) -> list[str]:
204+
return sorted(
205+
path for path in paths
206+
if not Path(path).parent.name.startswith("csep-synth-")
207+
)
208+
209+
196210
def _validate_changed(run_id: str, changed: list[str]) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
197211
valid: list[dict[str, Any]] = []
198212
invalid: list[dict[str, Any]] = []

‎tests/test_skill_synthesis_runner.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,23 @@ def fake_agent(**kwargs):
9999
assert result["status"] == "error"
100100
assert result["dry_run_leak"] is True
101101
assert result["changed_real_paths"] == [str(real_root / "csep-synth-leak" / "SKILL.md")]
102+
103+
104+
def test_run_skill_synthesis_rejects_non_synth_skill_writes(tmp_path: Path) -> None:
105+
_write_config(tmp_path)
106+
107+
def fake_agent(**kwargs):
108+
skill = Path(kwargs["skills_root"]) / "manual-skill" / "SKILL.md"
109+
skill.parent.mkdir(parents=True)
110+
skill.write_text(
111+
"---\nname: manual-skill\ndescription: Use when wrong namespace repeats.\n---\n\nBad write.\n",
112+
encoding="utf-8",
113+
)
114+
out = Path(kwargs["run_output_dir"]) / "result.json"
115+
out.parent.mkdir(parents=True, exist_ok=True)
116+
out.write_text(json.dumps({"schema_version": 1, "run_id": kwargs["run_id"], "actions": []}), encoding="utf-8")
117+
118+
result = run_skill_synthesis(home=tmp_path, mode="incremental", lookback_hours=24, lookback_days=None, dry_run=True, agent_invoker=fake_agent)
119+
120+
assert result["status"] == "error"
121+
assert result["namespace_violations"]

0 commit comments

Comments
 (0)