From 790448294e37c1e58d3daa20907ddcaf55e6dca6 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:58:58 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20address=20PR=20review=20round=202=20?= =?UTF-8?q?=E2=80=94=20legacy=20rmtree=20confirmation,=20agent=5Fpack=20fl?= =?UTF-8?q?ag,=20registrar=20alias,=20manifest=20ID=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Legacy rmtree: prompt user before deleting agent directory in legacy fallback path (both no-manifest and AgentPackError cases), respects --force - Set options['agent_pack'] = True during agent_switch so projects originally created with --ai reflect pack-based management after switch - Add cursor-agent alias in CommandRegistrar.AGENT_CONFIGS so extension re-registration works when switching to/from cursor-agent - Validate manifest.id matches agent_id in resolve_agent_pack() to prevent malicious override packs from injecting different IDs --- src/specify_cli/__init__.py | 13 +++++++++++++ src/specify_cli/agent_pack.py | 8 ++++++++ src/specify_cli/agents.py | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 3918963c..5282b198 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -2697,6 +2697,12 @@ def agent_switch( if agent_folder: agent_dir = project_path / agent_folder.rstrip("/") if agent_dir.is_dir(): + if not force: + console.print(f"[yellow]No install manifest found for '{current_agent}' (legacy project).[/yellow]") + console.print(f" Directory to remove: {agent_dir}") + if not typer.confirm("Remove this directory?"): + console.print("[dim]Aborted. Use --force to skip this check.[/dim]") + raise typer.Exit(0) shutil.rmtree(agent_dir) console.print(f" [green]✓[/green] {current_agent} directory removed (legacy)") else: @@ -2708,6 +2714,12 @@ def agent_switch( if agent_folder: agent_dir = project_path / agent_folder.rstrip("/") if agent_dir.is_dir(): + if not force: + console.print(f"[yellow]No agent pack found for '{current_agent}' (legacy project).[/yellow]") + console.print(f" Directory to remove: {agent_dir}") + if not typer.confirm("Remove this directory?"): + console.print("[dim]Aborted. Use --force to skip this check.[/dim]") + raise typer.Exit(0) shutil.rmtree(agent_dir) console.print(f" [green]✓[/green] {current_agent} directory removed (legacy)") @@ -2757,6 +2769,7 @@ def agent_switch( # Update init options options["ai"] = agent_id + options["agent_pack"] = True options.pop("agent_switch_error", None) # clear any previous error init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8") diff --git a/src/specify_cli/agent_pack.py b/src/specify_cli/agent_pack.py index 144a1a6e..78f6de88 100644 --- a/src/specify_cli/agent_pack.py +++ b/src/specify_cli/agent_pack.py @@ -721,6 +721,14 @@ def resolve_agent_pack( manifest_file = pack_dir / MANIFEST_FILENAME if manifest_file.is_file(): manifest = AgentManifest.from_yaml(manifest_file) + # Verify the manifest's declared ID matches the requested + # agent_id to prevent a malicious override from injecting + # a different ID (used for file paths and module names). + if manifest.id != agent_id: + raise PackResolutionError( + f"Agent pack manifest ID '{manifest.id}' does not match " + f"requested ID '{agent_id}' (in {pack_dir})." + ) if source == "embedded": embedded_manifest = manifest diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 4f1ab728..4165e3f3 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -47,6 +47,12 @@ class CommandRegistrar: "args": "$ARGUMENTS", "extension": ".md" }, + "cursor-agent": { + "dir": ".cursor/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md" + }, "qwen": { "dir": ".qwen/commands", "format": "markdown",