Skip to content

Commit 4ba10a2

Browse files
committed
fix(codex): harden lite sharing workflows
1 parent 6082486 commit 4ba10a2

6 files changed

Lines changed: 166 additions & 5 deletions

File tree

platform-integrations/codex/plugins/evolve-lite/skills/publish/scripts/publish.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def main():
8181
if not dest_path.is_relative_to(dest_base):
8282
print(f"Error: invalid entity name: {args.entity!r}", file=sys.stderr)
8383
sys.exit(1)
84+
if dest_path.exists():
85+
print(f"Error: already published: {dest_path}\nUnpublish it first or delete it manually.", file=sys.stderr)
86+
sys.exit(1)
8487
dest_path.write_text(entity_to_markdown(entity), encoding="utf-8")
8588
src_path.unlink()
8689

platform-integrations/codex/plugins/evolve-lite/skills/subscribe/scripts/subscribe.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def main():
3838
project_root = str(evolve_dir.resolve()) if evolve_dir.name != ".evolve" else str(evolve_dir.resolve().parent)
3939
subscribed_base = (evolve_dir / "subscribed").resolve()
4040
dest = (evolve_dir / "subscribed" / args.name).resolve()
41-
if not dest.is_relative_to(subscribed_base):
41+
if args.name in {"", "."} or dest == subscribed_base or not dest.is_relative_to(subscribed_base):
4242
print(f"Error: invalid subscription name: {args.name!r}", file=sys.stderr)
4343
sys.exit(1)
4444

@@ -53,7 +53,8 @@ def main():
5353
sys.exit(1)
5454

5555
if dest.exists():
56-
print(f"Warning: {dest} already exists, skipping clone.", file=sys.stderr)
56+
print(f"Error: destination already exists: {dest}", file=sys.stderr)
57+
sys.exit(1)
5758
else:
5859
dest.parent.mkdir(parents=True, exist_ok=True)
5960
subprocess.run(

platform-integrations/codex/plugins/evolve-lite/skills/sync/scripts/sync.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def copy_entities(subscribed_repo_path, entities_subscribed_path):
4242
if entities_subscribed_path.exists():
4343
shutil.rmtree(entities_subscribed_path)
4444
for md in sorted(subscribed_repo_path.glob("**/*.md")):
45+
if md.is_symlink():
46+
continue
4547
rel = md.relative_to(subscribed_repo_path)
4648
dest = entities_subscribed_path / rel
4749
dest.parent.mkdir(parents=True, exist_ok=True)
@@ -78,6 +80,11 @@ def main():
7880
parser = argparse.ArgumentParser()
7981
parser.add_argument("--quiet", action="store_true", help="Suppress output if no changes")
8082
parser.add_argument("--config", default=None, help="Explicit config path")
83+
parser.add_argument(
84+
"--session-start",
85+
action="store_true",
86+
help="Apply session-start gating for automatic hook execution",
87+
)
8188
args = parser.parse_args()
8289

8390
evolve_dir = Path(os.environ.get("EVOLVE_DIR", ".evolve"))
@@ -90,7 +97,7 @@ def main():
9097
cfg = load_config(project_root)
9198

9299
sync_cfg = cfg.get("sync", {})
93-
if isinstance(sync_cfg, dict) and sync_cfg.get("on_session_start") is False:
100+
if args.session_start and isinstance(sync_cfg, dict) and sync_cfg.get("on_session_start") is False:
94101
sys.exit(0)
95102

96103
subscriptions = cfg.get("subscriptions", [])

platform-integrations/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def _codex_sync_hook_command():
381381
'd=\"$PWD\"; '
382382
"while :; do "
383383
'candidate=\"$d/plugins/evolve-lite/skills/sync/scripts/sync.py\"; '
384-
'if [ -f \"$candidate\" ]; then EVOLVE_DIR=\"$d/.evolve\" exec python3 \"$candidate\" --quiet; fi; '
384+
'if [ -f \"$candidate\" ]; then EVOLVE_DIR=\"$d/.evolve\" exec python3 \"$candidate\" --quiet --session-start; fi; '
385385
'[ \"$d\" = \"/\" ] && break; '
386386
'd=\"$(dirname \"$d\")\"; '
387387
"done; "

tests/platform_integrations/test_codex.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def test_install_creates_expected_files(self, temp_project_dir, install_runner,
113113
'd="$PWD"; '
114114
"while :; do "
115115
'candidate="$d/plugins/evolve-lite/skills/sync/scripts/sync.py"; '
116-
'if [ -f "$candidate" ]; then EVOLVE_DIR="$d/.evolve" exec python3 "$candidate" --quiet; fi; '
116+
'if [ -f "$candidate" ]; then EVOLVE_DIR="$d/.evolve" exec python3 "$candidate" --quiet --session-start; fi; '
117117
'[ "$d" = "/" ] && break; '
118118
'd="$(dirname "$d")"; '
119119
"done; "

tests/platform_integrations/test_codex_sharing.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ def test_publish_succeeds_without_user_flag(self, temp_project_dir):
169169
content = (temp_project_dir / ".evolve" / "public" / "guideline" / "my-tip.md").read_text()
170170
assert "visibility: public" in content
171171

172+
def test_publish_fails_when_public_entity_already_exists(self, temp_project_dir):
173+
guideline_dir = temp_project_dir / ".evolve" / "entities" / "guideline"
174+
guideline_dir.mkdir(parents=True)
175+
source = guideline_dir / "my-tip.md"
176+
source.write_text("---\ntype: guideline\n---\n\nPrefer composition.\n")
177+
178+
public_path = temp_project_dir / ".evolve" / "public" / "guideline" / "my-tip.md"
179+
public_path.parent.mkdir(parents=True)
180+
existing_content = "---\ntype: guideline\nvisibility: public\n---\n\nExisting public content.\n"
181+
public_path.write_text(existing_content)
182+
183+
result = run_script(
184+
PUBLISH_SCRIPT,
185+
project_dir=temp_project_dir,
186+
args=["--entity", "my-tip.md"],
187+
evolve_dir=temp_project_dir / ".evolve",
188+
expect_success=False,
189+
)
190+
191+
assert result.returncode != 0
192+
assert "already published" in result.stderr
193+
assert public_path.read_text() == existing_content
194+
assert source.exists()
195+
172196
def test_publish_rejects_path_traversal_in_entity_name(self, temp_project_dir):
173197
guideline_dir = temp_project_dir / ".evolve" / "entities" / "guideline"
174198
guideline_dir.mkdir(parents=True)
@@ -240,6 +264,37 @@ def test_subscribe_rejects_path_traversal_in_name(self, temp_project_dir, local_
240264
assert result.returncode != 0
241265
assert "invalid subscription name" in result.stderr
242266

267+
@pytest.mark.parametrize("name", ["", "."])
268+
def test_subscribe_rejects_empty_or_dot_name(self, temp_project_dir, local_repo, name):
269+
result = run_script(
270+
SUBSCRIBE_SCRIPT,
271+
project_dir=temp_project_dir,
272+
args=["--name", name, "--remote", str(local_repo["bare"]), "--branch", "main"],
273+
evolve_dir=temp_project_dir / ".evolve",
274+
expect_success=False,
275+
)
276+
277+
assert result.returncode != 0
278+
assert "invalid subscription name" in result.stderr
279+
280+
def test_subscribe_fails_when_destination_already_exists(self, temp_project_dir, local_repo):
281+
evolve_dir = temp_project_dir / ".evolve"
282+
existing_dest = evolve_dir / "subscribed" / "alice"
283+
existing_dest.mkdir(parents=True)
284+
285+
result = run_script(
286+
SUBSCRIBE_SCRIPT,
287+
project_dir=temp_project_dir,
288+
args=["--name", "alice", "--remote", str(local_repo["bare"]), "--branch", "main"],
289+
evolve_dir=evolve_dir,
290+
expect_success=False,
291+
)
292+
293+
assert result.returncode != 0
294+
assert "destination already exists" in result.stderr
295+
config_path = temp_project_dir / "evolve.config.yaml"
296+
assert not config_path.exists()
297+
243298
def test_unsubscribe_list_and_not_found(self, temp_project_dir, local_repo):
244299
evolve_dir = temp_project_dir / ".evolve"
245300
run_script(
@@ -315,6 +370,78 @@ def test_sync_no_subscriptions_exits_cleanly(self, temp_project_dir):
315370
assert "No subscriptions" in result.stdout
316371
assert "evolve-lite:subscribe" in result.stdout
317372

373+
def test_sync_skips_invalid_subscription_name(self, temp_project_dir):
374+
evolve_dir = temp_project_dir / ".evolve"
375+
(temp_project_dir / "evolve.config.yaml").write_text(
376+
'subscriptions:\n'
377+
' - name: "."\n'
378+
' remote: "https://example.com/repo.git"\n'
379+
' branch: "main"\n'
380+
)
381+
382+
result = run_script(
383+
SYNC_SCRIPT,
384+
project_dir=temp_project_dir,
385+
evolve_dir=evolve_dir,
386+
expect_success=False,
387+
)
388+
389+
assert result.returncode == 0
390+
assert ". (invalid subscription name)" in result.stdout
391+
assert not (evolve_dir / "entities" / "subscribed").exists()
392+
393+
def test_manual_sync_runs_even_when_on_session_start_is_false(self, temp_project_dir, local_repo):
394+
evolve_dir = temp_project_dir / ".evolve"
395+
run_script(
396+
SUBSCRIBE_SCRIPT,
397+
project_dir=temp_project_dir,
398+
args=["--name", "alice", "--remote", str(local_repo["bare"]), "--branch", "main"],
399+
evolve_dir=evolve_dir,
400+
)
401+
(temp_project_dir / "evolve.config.yaml").write_text(
402+
'subscriptions:\n'
403+
f' - name: "alice"\n remote: "{local_repo["bare"]}"\n branch: "main"\n'
404+
'sync:\n'
405+
' on_session_start: false\n'
406+
)
407+
408+
result = run_script(
409+
SYNC_SCRIPT,
410+
project_dir=temp_project_dir,
411+
evolve_dir=evolve_dir,
412+
expect_success=False,
413+
)
414+
415+
assert result.returncode == 0
416+
assert "Synced 1 repo(s):" in result.stdout
417+
418+
def test_session_start_sync_respects_on_session_start_false(self, temp_project_dir, local_repo):
419+
evolve_dir = temp_project_dir / ".evolve"
420+
run_script(
421+
SUBSCRIBE_SCRIPT,
422+
project_dir=temp_project_dir,
423+
args=["--name", "alice", "--remote", str(local_repo["bare"]), "--branch", "main"],
424+
evolve_dir=evolve_dir,
425+
)
426+
(temp_project_dir / "evolve.config.yaml").write_text(
427+
'subscriptions:\n'
428+
f' - name: "alice"\n remote: "{local_repo["bare"]}"\n branch: "main"\n'
429+
'sync:\n'
430+
' on_session_start: false\n'
431+
)
432+
433+
result = run_script(
434+
SYNC_SCRIPT,
435+
project_dir=temp_project_dir,
436+
args=["--quiet", "--session-start"],
437+
evolve_dir=evolve_dir,
438+
expect_success=False,
439+
)
440+
441+
assert result.returncode == 0
442+
mirrored = evolve_dir / "entities" / "subscribed" / "alice" / "guideline" / "tip-one.md"
443+
assert not mirrored.exists()
444+
318445
def test_sync_writes_audit_log(self, temp_project_dir, local_repo):
319446
evolve_dir = temp_project_dir / ".evolve"
320447
run_script(
@@ -351,6 +478,29 @@ def test_sync_picks_up_new_entity_after_push(self, temp_project_dir, local_repo)
351478
assert mirrored.exists()
352479
assert "Delete dead code promptly." in mirrored.read_text()
353480

481+
def test_sync_skips_symlinked_markdown_files(self, temp_project_dir, local_repo):
482+
evolve_dir = temp_project_dir / ".evolve"
483+
run_script(
484+
SUBSCRIBE_SCRIPT,
485+
project_dir=temp_project_dir,
486+
args=["--name", "alice", "--remote", str(local_repo["bare"]), "--branch", "main"],
487+
evolve_dir=evolve_dir,
488+
)
489+
490+
git_env = local_repo["env"]
491+
target = local_repo["work"] / "guideline" / "tip-one.md"
492+
symlink = local_repo["work"] / "guideline" / "tip-link.md"
493+
symlink.symlink_to(target.name)
494+
subprocess.run(["git", "-C", str(local_repo["work"]), "add", "."], check=True, env=git_env)
495+
subprocess.run(["git", "-C", str(local_repo["work"]), "commit", "-m", "add tip symlink"], check=True, env=git_env)
496+
subprocess.run(["git", "-C", str(local_repo["work"]), "push", "origin", "main"], check=True, env=git_env)
497+
498+
run_script(SYNC_SCRIPT, project_dir=temp_project_dir, evolve_dir=evolve_dir)
499+
500+
mirrored_dir = evolve_dir / "entities" / "subscribed" / "alice" / "guideline"
501+
assert (mirrored_dir / "tip-one.md").exists()
502+
assert not (mirrored_dir / "tip-link.md").exists()
503+
354504
def test_sync_removed_entity_disappears_after_sync(self, temp_project_dir, local_repo):
355505
evolve_dir = temp_project_dir / ".evolve"
356506
run_script(

0 commit comments

Comments
 (0)