[PPSC-697] feat: agent discovery#150
Conversation
Test Coverage Reporttotal: (statements) 77.2% Coverage by function |
There was a problem hiding this comment.
Pull request overview
Adds a new armis-cli agent-detection command to scan user profiles for installed AI coding agents and report whether Armis AppSec MCP is configured, and extends install to support additional editor targets.
Changes:
- Introduces
internal/agentdetectscanning framework with per-agent detectors, platform-specific user/profile discovery, and plain/JSON output formatters. - Adds the
agent-detectionCobra command wiring and output format flag validation. - Extends installable editor targets to include Roo Code and Junie, and documents OpenHands as manual-only.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/install/editors.go | Adds Roo Code/Junie as auto-configurable editors and their default config paths. |
| internal/cmd/install.go | Updates install help text and adds manual guidance for OpenHands. |
| internal/cmd/agent_detection.go | Adds new agent-detection CLI command with --format support. |
| internal/agentdetect/platform.go | Defines the Platform interface and UserHome model for cross-OS scanning. |
| internal/agentdetect/platform_darwin.go | macOS implementation for user enumeration and editor-specific paths. |
| internal/agentdetect/platform_linux.go | Linux implementation for user enumeration and editor-specific paths. |
| internal/agentdetect/platform_windows.go | Windows implementation for user enumeration and editor-specific paths + admin detection. |
| internal/agentdetect/userprofile.go | Shared helpers for enumerating home directories and JetBrains plugin paths. |
| internal/agentdetect/agent.go | Defines agent names and the detector registry. |
| internal/agentdetect/agentdetect.go | Implements the scanner and result models (grouped + flat). |
| internal/agentdetect/detector.go | Implements per-agent detection, MCP checks, and some version extraction. |
| internal/agentdetect/mcpconfig.go | Implements MCP presence checks for multiple config formats. |
| internal/agentdetect/format.go | Implements plain and json output writers. |
| internal/agentdetect/agentdetect_test.go | Unit tests for scanner aggregation and output formatters. |
| internal/agentdetect/detector_test.go | Unit tests for each detector + path traversal safety checks. |
| internal/agentdetect/mcpconfig_test.go | Unit tests for MCP config parsing helpers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if _, err := fmt.Fprintf(w, "user: %s\n", userResult.User); err != nil { | ||
| return err | ||
| } | ||
| agents := make([]string, 0, len(userResult.Agents)) | ||
| for _, agent := range userResult.Agents { | ||
| agents = append(agents, fmt.Sprintf("%s(MCP:%t)", agent.Name, agent.MCPInstalled)) | ||
| } | ||
| if _, err := fmt.Fprintln(w, strings.Join(agents, ", ")); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
The PR description says the plain output is "one line per user", but this formatter prints two lines per user (a "user:" header line plus a second line listing agents). Either adjust the plain formatter to emit a single line per user, or update the command/README text to match the actual output format so downstream parsing expectations don't break.
| if err != nil { | ||
| return false | ||
| } | ||
| return strings.HasPrefix(resolved, resolvedBase+string(filepath.Separator)) |
There was a problem hiding this comment.
isUnderDir relies on a case-sensitive strings.HasPrefix comparison. On Windows (case-insensitive paths), this can produce false negatives if resolvedBase and resolved differ only by drive letter or casing, which would cause agent detection to miss valid paths. Consider switching to a filepath.Rel-based check (rel not starting with ".." and not ".") and, on Windows, normalizing/strings.EqualFold-ing the volume/case before comparison.
| return strings.HasPrefix(resolved, resolvedBase+string(filepath.Separator)) | |
| baseVol := filepath.VolumeName(resolvedBase) | |
| resolvedVol := filepath.VolumeName(resolved) | |
| if baseVol != "" && resolvedVol != "" && strings.EqualFold(baseVol, resolvedVol) { | |
| resolved = baseVol + strings.TrimPrefix(resolved, resolvedVol) | |
| } | |
| rel, err := filepath.Rel(resolvedBase, resolved) | |
| if err != nil { | |
| return false | |
| } | |
| return rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) |
| } | ||
|
|
||
| func (d *clineDetector) CheckMCP(resolvedHome, homeDir string, _ Platform) bool { | ||
| return HasArmisMCP(resolvedHome, filepath.Join(homeDir, ".cline", "mcp_settings.json")) |
There was a problem hiding this comment.
The clineDetector.CheckMCP() reads ~/.cline/mcp_settings.json, but armis-cli install cline writes to ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json. After a user installs and then runs detection, Cline will always report MCP:false — the two features disagree on where Cline stores its config.
There was a problem hiding this comment.
There's quite a bit of repeat here, I suggest defining an AgentConfig struct with Name, ConfigDirs []string, MCPConfigPath string, ExtensionPrefix string fields. Implement a generic detector that reads from config. Keep custom detectors (Claude Code's enabledPlugins, Continue's directory scanning) as special cases.
There was a problem hiding this comment.
Good point, there's definitely repetition here. I looked into a config-driven approach but ~half the agents have enough quirks (Claude's enabledPlugins, Continue's directory scanning, Zed's context_servers, Copilot reading from VSCode's config dir, etc.) that the generic struct would need several escape hatches. I'd prefer to keep the explicit detectors for now and revisit if/when we add more agents. Happy to discuss if you feel strongly about it though.
| result := &ScanResult{} | ||
| for _, user := range users { | ||
| resolvedHome, err := resolvePath(user.HomeDir) | ||
| if err != nil { |
There was a problem hiding this comment.
When resolvePath(user.HomeDir) fails, the scan silently continues without any indication. Maybe add a Skipped []SkippedUser field to ScanResult or log warnings to stderr: fmt.Fprintf(os.Stderr, "warning: skipping user %s: %v\n", user.Username, err).?
There was a problem hiding this comment.
The plain formatter outputs raw fmt.Fprintf text while every other user-facing output in the project uses the lipgloss styling pipeline (documented in CLAUDE.md). The output looks disconnected from the rest of the CLI.
| return false | ||
| } | ||
| for _, entry := range entries { | ||
| if strings.Contains(strings.ToLower(entry.Name()), "armis") { |
There was a problem hiding this comment.
This seems to be filename-based only, while every other detector parses JSON content. A stale/empty armis-old.json or a armis-appsec.json.bak editor backup will falsely report "MCP installed = true".
am i missing something?
Related Issue
Type of Change
Problem
Armis Cloud has no visibility into which AI coding agents (Claude Code, Cursor, Copilot, Windsurf, etc.) are installed across developer workstations, nor whether those agents have the Armis AppSec MCP server configured. This data is needed to track agent adoption and MCP deployment coverage.
Solution
Add a new
armis-cli agent-detectioncommand that scans the local filesystem for 15 AI coding agents and reports whether each has Armis AppSec MCP installed.Agent detection (
internal/agentdetect/):Output formats:
--format plain(default): grouped by user, one line per user with agents and MCP status--format json: flat array ofDetectedAgentobjectsInstall command updates:
Testing
Automated Tests
Manual Testing
armis-cli agent-detection— verified detection of locally installed agents (Claude Code, Cursor) with correct MCP statussudo armis-cli agent-detection --format json— scan all users on the machine, JSON outputarmis-cli install roocode/armis-cli install junie— verified new editor targets workReviewer Notes
Checklist