Make ACP discovery only report actually-installed agents.

c6e7c8a94044 · AtlantisPleb · · parent 2e59e30d513a

Make ACP discovery only report actually-installed agents.

- Stop treating the presence of `npx` or `uvx` on PATH as proof that every
  npx/uvx-distributed agent is installed.
- For npx agents, verify the package exists under `npm root -g`.
- For uvx agents, verify the package appears in `uv tool list`.
- Also allow an executable whose name matches the agent id to count as found.
- Cache `npm root -g` and `uv tool list` once per `find_agents` call.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/acp.rs

Diff

1 file changed, +80 -5

crates/coder-lite/src/acp.rs modified +80 -5

@@ -35,6 +35,10 @@ pub async fn find_agents() -> Result<Vec<Agent>, Box<dyn std::error::Error>> {

35 35
    let mut entries = tokio::fs::read_dir(&dir).await?;
36 36
    let mut found = Vec::new();
37 37
38
    // Compute these once; many agents use npx or uvx, so avoid repeated calls.
39
    let npm_root = npm_root().await;
40
    let uv_tools = uv_tools().await;
41
38 42
    while let Some(entry) = entries.next_entry().await? {
39 43
        let path = entry.path();
40 44
        if !path.is_dir() {

@@ -52,7 +56,7 @@ pub async fn find_agents() -> Result<Vec<Agent>, Box<dyn std::error::Error>> {

52 56
            Err(_) => continue,
53 57
        };
54 58
55
        if is_available(&agent).await {
59
        if is_available(&agent, &npm_root, &uv_tools).await {
56 60
            found.push(Agent {
57 61
                id: agent.id,
58 62
                name: agent.name,

@@ -102,11 +106,12 @@ struct NpxUvx {

102 106
    args: Option<Vec<String>>,
103 107
}
104 108
105
async fn is_available(agent: &RegistryAgent) -> bool {
109
async fn is_available(agent: &RegistryAgent, npm_root: &Option<String>, uv_tools: &Option<String>) -> bool {
106 110
    let Some(dist) = &agent.distribution else {
107 111
        return false;
108 112
    };
109 113
114
    // Binary distribution for this platform.
110 115
    if let Some(binary) = &dist.binary {
111 116
        let platform = current_platform();
112 117
        if let Some(target) = binary.get(&platform) {

@@ -120,12 +125,23 @@ async fn is_available(agent: &RegistryAgent) -> bool {

120 125
        }
121 126
    }
122 127
123
    if dist.npx.is_some() && has_command("npx").await {
128
    // An executable named exactly like the agent id (e.g., `grok-build`).
129
    if has_command(&agent.id).await {
124 130
        return true;
125 131
    }
126 132
127
    if dist.uvx.is_some() && has_command("uvx").await {
128
        return true;
133
    // npx package installed globally.
134
    if let Some(npx) = &dist.npx {
135
        if npx_installed(&npx.package, npm_root).await {
136
            return true;
137
        }
138
    }
139
140
    // uvx package installed as a uv tool.
141
    if let Some(uvx) = &dist.uvx {
142
        if uvx_installed(&uvx.package, uv_tools).await {
143
            return true;
144
        }
129 145
    }
130 146
131 147
    false

@@ -159,6 +175,65 @@ async fn has_command(name: &str) -> bool {

159 175
    }
160 176
}
161 177
178
async fn npm_root() -> Option<String> {
179
    let output = tokio::process::Command::new("npm")
180
        .args(["root", "-g"])
181
        .output()
182
        .await;
183
    output
184
        .ok()
185
        .filter(|o| o.status.success())
186
        .and_then(|o| String::from_utf8(o.stdout).ok())
187
        .map(|s| s.trim().to_string())
188
}
189
190
async fn uv_tools() -> Option<String> {
191
    let output = tokio::process::Command::new("uv")
192
        .args(["tool", "list"])
193
        .output()
194
        .await;
195
    output
196
        .ok()
197
        .filter(|o| o.status.success())
198
        .and_then(|o| String::from_utf8(o.stdout).ok())
199
}
200
201
async fn npx_installed(package: &str, npm_root: &Option<String>) -> bool {
202
    let Some(root) = npm_root else {
203
        return false;
204
    };
205
    let package = package_dir(package);
206
    let marker = Path::new(root)
207
        .join("node_modules")
208
        .join(package)
209
        .join("package.json");
210
    tokio::fs::metadata(&marker).await.map(|m| m.is_file()).unwrap_or(false)
211
}
212
213
async fn uvx_installed(package: &str, uv_tools: &Option<String>) -> bool {
214
    let Some(list) = uv_tools else {
215
        return false;
216
    };
217
    let package = package_dir(package);
218
    list.lines().any(|line| {
219
        line.split_whitespace().next() == Some(package)
220
    })
221
}
222
223
fn package_dir(spec: &str) -> &str {
224
    // Strip an `@<version>` suffix while preserving scoped package names.
225
    // "@scope/pkg@1.0.0" -> "@scope/pkg", "pkg@1.0.0" -> "pkg".
226
    spec.rsplit_once('@')
227
        .and_then(|(left, right)| {
228
            if right.chars().next().map_or(false, |c| c.is_ascii_digit()) {
229
                Some(left)
230
            } else {
231
                None
232
            }
233
        })
234
        .unwrap_or(spec)
235
}
236
162 237
fn home_dir() -> Option<PathBuf> {
163 238
    std::env::var_os("HOME")
164 239
        .or_else(|| std::env::var_os("USERPROFILE"))

This page updates live while a promote is in flight · changelog