Add multi-framework adapters and auto-discovery for 12 AI agents#68
Conversation
Shared data model for auto-discovery and multi-framework scanning. Defines marker paths, config paths, MCP config paths, and binary names for Claude Code, Claude Desktop, Cursor, Windsurf, VS Code Copilot, Gemini CLI, OpenAI Codex, OpenClaw, Amazon Q, Kiro, Continue, and Roo Code across macOS, Linux, and Windows. Signed-off-by: debu-sinha <debusinha2009@gmail.com>
Framework adapters (#55): FrameworkAdapter ABC with typed FrameworkConfig dataclass. Adapters for Claude Code, Cursor, Windsurf, and VS Code Copilot normalize configs into a unified model for cross-framework security checks. Auto-discovery (#53): discover_agents() scans filesystem for 12 known AI agent installations. CLI command: agentsec discover. Version detection opt-in via --detect-versions flag. New files: adapters/base.py (157), adapters/claude_code.py (485), adapters/cursor.py (391), adapters/windsurf.py (261), adapters/vscode_copilot.py (310), discovery.py (206), models/agents.py (401). Total: 2,211 lines. Signed-off-by: debu-sinha <debusinha2009@gmail.com>
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix unreachable except blocks caused by ordering, place more specific exceptions before more general ones, or remove the redundant handler if special handling is not needed. Here, _read_json returns None for all error conditions; the only difference between the OSError and PermissionError handlers is the log message. _read_text in the same file has the same structural issue and also treats errors uniformly, with only logging differing.
The simplest fix that preserves existing behavior is to remove the unreachable except PermissionError: blocks entirely. All PermissionErrors are already handled by the OSError clause (as PermissionError is a subclass of OSError), which logs an error-specific message and returns None. Deleting the redundant blocks does not change runtime behavior but eliminates the CodeQL warning and the dead code. Concretely:
- In
_read_json(around lines 70–78), delete theexcept PermissionError:block. - In
_read_text(around lines 85–90), delete theexcept PermissionError:block.
No new imports or helper methods are needed.
| @@ -73,9 +73,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| def _read_text(path: Path) -> str | None: | ||
| @@ -85,9 +82,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| class ClaudeCodeAdapter(FrameworkAdapter): |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix unreachable except blocks where a more specific exception comes after a more general one, you either (a) reorder the except clauses so the more specific exception is listed first, or (b) remove the redundant specific handler if it doesn’t need distinct behavior. The goal is to ensure each except block is either reachable or eliminated.
For this codebase, the best way to fix the problem without changing existing functionality is to mirror the pattern already used in _read_json. In _read_json, PermissionError is handled after OSError, which is technically redundant/unreachable as well. However, functionally, both _read_json and _read_text simply log a debug message and return None for both OSError and PermissionError. Since _read_text’s PermissionError handler logs a slightly different message, but PermissionError is already subsumed by OSError, the simplest non‑behavior‑changing fix is to remove the unreachable except PermissionError: block from _read_text and let PermissionError be handled by the existing OSError block. This preserves overall behavior except for the exact debug message for permission errors in _read_text, which currently never executes anyway, so there is no user-visible behavior to preserve.
Concretely:
- Edit
src/agentsec/adapters/claude_code.py. - In
_read_text, delete theexcept PermissionError:clause and its body (lines 88–90 in the snippet). - Leave
_read_jsonunchanged, since the issue reported is specifically for_read_text, and we must minimize changes.
No new imports, methods, or other definitions are required.
| @@ -85,11 +85,9 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
|
|
||
| class ClaudeCodeAdapter(FrameworkAdapter): | ||
| """Adapter for Claude Code agent framework configuration.""" | ||
|
|
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, this issue is fixed by making sure more specific except clauses (for subclasses) come before more general ones (for superclasses), or by removing redundant handlers that are subsumed by a more general one. Here, both the OSError and PermissionError handlers in _read_json only log a debug message and return None, so there is no functional distinction between them. The same is true in _read_text. Therefore, the minimal, behavior-preserving change is to remove the unreachable except PermissionError: blocks in both functions and rely on the existing OSError handler to cover PermissionError as well.
Concretely:
- In
src/agentsec/adapters/cursor.py, within_read_json, delete lines 61–63 (except PermissionError:and its body). - In the same file, within
_read_text, delete lines 73–75 (except PermissionError:and its body).
No imports or new helpers are required; we only remove dead code. All errors, including permission issues, will still be logged (via the OSError handler) and result in None, which matches the current public behavior.
| @@ -58,9 +58,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| def _read_text(path: Path) -> str | None: | ||
| @@ -70,9 +67,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| def _parse_mdc(content: str, source_file: str) -> RuleConfig | None: |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix unreachable except blocks caused by ordering, you either (1) reorder the handlers to put more specific exceptions before more general ones, or (2) remove the redundant handler if its behavior is not needed or is already covered by the more general block.
Here, _read_text already logs and returns None in both the OSError and PermissionError handlers, so the PermissionError block is redundant; the OSError handler already covers it with identical behavior. The simplest fix that preserves functionality is to delete the unreachable except PermissionError: clause and let PermissionError be handled by the OSError handler.
Concretely, in src/agentsec/adapters/cursor.py, within the _read_text function, remove lines 73–75:
except PermissionError:
logger.debug("Permission denied reading %s", path)
return NoneNo new imports or helper methods are required.
| @@ -70,9 +70,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| def _parse_mdc(content: str, source_file: str) -> RuleConfig | None: |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix unreachable except blocks, ensure that more specific exception types appear before more general ones, or remove redundant handlers if their behavior is identical to the broader handler.
In this file, _read_json currently has three handlers: json.JSONDecodeError, OSError, and PermissionError. Because PermissionError is a subclass of OSError, the except PermissionError: block is unreachable. There are two reasonable options:
- If you want distinct logging for permission issues, move the
PermissionErrorhandler above theOSErrorhandler. - If you are fine with permission errors being treated like other
OSErrors, remove the dedicatedPermissionErrorhandler.
The _read_text function below already follows pattern (2): it only has an OSError handler and no separate PermissionError handler. To keep behavior consistent across helpers and avoid changing runtime behavior, the minimal, safest fix is to remove the unreachable except PermissionError: block from _read_json, leaving OSError to handle all filesystem-related errors as it already does. No new imports or helper methods are needed; only the body of _read_json in src/agentsec/adapters/vscode_copilot.py should be edited.
| @@ -55,9 +55,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| def _read_text(path: Path) -> str | None: |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix an unreachable except block where a subclass-specific handler appears after a superclass handler, you either reorder the handlers so the more specific one comes first, or remove the redundant handler if the specific handling is not needed. The choice depends on whether the code truly needs distinct behavior for that subclass.
Here, _read_text currently logs all OSError (including permission errors) via a generic message, and then has an unreachable PermissionError handler with a more specific log message. The simplest fix that preserves intent and maintains consistency with _read_json is to reverse the order of the two handlers: first catch PermissionError, then catch any remaining OSError. This keeps behavior for non-permission OSError unchanged and enables the more precise logging for permission errors, matching the pattern already used in _read_json.
Concretely, in src/agentsec/adapters/vscode_copilot.py, in the _read_text function, swap the order of the except OSError as e: and except PermissionError: blocks, without changing their bodies. No new imports or helper methods are required; we only reorder the existing except clauses.
| @@ -64,12 +64,12 @@ | ||
| """Read a text file, returning None on any error.""" | ||
| try: | ||
| return path.read_text(errors="replace") | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
|
|
||
|
|
||
| class VSCodeCopilotAdapter(FrameworkAdapter): |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix an unreachable except block caused by ordering, you must ensure that more specific exceptions (subclasses) are listed before more general ones (superclasses), or remove the redundant specific handler if it is not needed. Python will then match the specific handler first, falling back to the general one when appropriate.
For this file, the minimal, behavior-preserving fix is to reorder the two except clauses in _read_json so that except PermissionError: appears before except OSError as e:. This change does not alter the return value (None in both cases) but makes the specialized logging for permission errors actually execute when a PermissionError occurs. No additional imports or helper methods are needed, and no other parts of the file need to change. _read_text already has the same structural issue (PermissionError after OSError), so it should be fixed in the same way for consistency and to avoid future similar alerts.
Concretely:
- In
src/agentsec/adapters/windsurf.py, in_read_json, swap the order of theexcept OSError as e:andexcept PermissionError:blocks. - In
_read_text, swap the order of itsexcept OSError as e:andexcept PermissionError:blocks as well.
These edits keep existing functionality while eliminating unreachable code and aligning with the recommendation to put specific handlers first.
| @@ -50,24 +50,24 @@ | ||
| except json.JSONDecodeError: | ||
| logger.debug("Malformed JSON in %s", path) | ||
| return None | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
|
|
||
|
|
||
| def _read_text(path: Path) -> str | None: | ||
| """Read a text file, returning None on any error.""" | ||
| try: | ||
| return path.read_text(errors="replace") | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
|
|
||
|
|
||
| class WindsurfAdapter(FrameworkAdapter): |
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: |
Check failure
Code scanning / CodeQL
Unreachable `except` block Error
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
In general, to fix unreachable except blocks caused by ordering, either (1) reorder the except clauses so that more specific exceptions come before more general ones, or (2) remove the redundant except clause if its behavior is effectively covered by the more general handler.
Here, _read_text’s except PermissionError: logs essentially the same type of informational message (“Permission denied reading …”) as _read_json does, but _read_text already has an except OSError as e: that logs a generic “Could not read …” message. The simplest, behavior-preserving change is to remove the except PermissionError: block from _read_text, letting PermissionError be handled by the existing OSError block. This mirrors the already-accepted behavior in _read_json, where PermissionError is also a subclass of OSError and thus handled by the OSError clause; the only functional difference is the exact log message, and currently _read_text never reaches its PermissionError block anyway.
Concretely:
- Edit
src/agentsec/adapters/windsurf.py. - In
_read_text, delete theexcept PermissionError:clause and its body (lines 68–70 in the provided snippet). - No new imports or helper methods are needed.
| @@ -65,9 +65,6 @@ | ||
| except OSError as e: | ||
| logger.debug("Could not read %s: %s", path, e) | ||
| return None | ||
| except PermissionError: | ||
| logger.debug("Permission denied reading %s", path) | ||
| return None | ||
|
|
||
|
|
||
| class WindsurfAdapter(FrameworkAdapter): |
Signed-off-by: debu-sinha <debusinha2009@gmail.com>
test_adapters.py: 64 tests covering detect, discover_configs, parse for Claude Code, Cursor, Windsurf, and VS Code Copilot adapters. test_discovery.py: 14 tests for discover_agents and helper functions. test_agent_definitions.py: 84 parametrized tests for agent registry. Signed-off-by: debu-sinha <debusinha2009@gmail.com>
| config = claude_adapter.parse(tmp_path) | ||
| assert config.sandbox is not None | ||
| assert config.sandbox.enabled is True | ||
| assert "api.example.com" in config.sandbox.network_allowed_domains |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
Copilot Autofix
AI about 2 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
Signed-off-by: debu-sinha <debusinha2009@gmail.com>
Signed-off-by: debu-sinha <debusinha2009@gmail.com>
Closes #53, closes #55
What changed
Multi-Framework Adapters (#55)
adapters/base.py: FrameworkAdapter ABC + FrameworkConfig with 6 typed sub-configsadapters/claude_code.py: Claude Code adapter (settings.json, hooks, .mcp.json, CLAUDE.md, rules, agents, commands)adapters/cursor.py: Cursor adapter (.cursorrules, .cursor/rules/*.mdc, mcp.json, tasks.json auto-run detection)adapters/windsurf.py: Windsurf adapter (.windsurfrules, mcp_config.json, dual path ~/.windsurf + ~/.codeium/windsurf)adapters/vscode_copilot.py: VS Code Copilot adapter (copilot-instructions.md, AGENTS.md, .vscode/mcp.json with "servers" key, tasks.json)Auto-Discovery (#53)
models/agents.py: AgentDefinition registry for 12 agents (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code Copilot, Gemini CLI, OpenAI Codex, OpenClaw, Amazon Q, Kiro, Continue, Roo Code) with OS-specific pathsdiscovery.py: Filesystem marker scanning, binary detection, MCP config enumerationcli.py: Addedagentsec discovercommand with --detect-versions and --json flagsArchitecture decisions
Per expert review:
Testing
Lint passes (ruff check + ruff format). Pyright unresolved imports are expected (agentsec not in IDE venv). Manual testing of discovery and adapters needed.