This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git

commit dc9a209e700194ec0f124fc5f47a57262e0cd864
Author: Yang Luo <[email protected]>
AuthorDate: Wed Aug 26 00:36:48 2026 +0800

    feat(agent): discover an agent by the state directory it left behind
    
    Discovery matched executables only: a launcher on PATH, an npm package,
    a winget or homebrew entry. An agent installed some other way was
    reported as not installed even with its home directory sitting there,
    which is what happened to Claude Code — the agent the README opens with.
    
    A fingerprint already names that directory, but only to read a version
    out of it. Treat a non-empty one as its own signal, and record it as an
    installation with no launcher: Start stays disabled through the CanStart
    it already reports, while providers, skills and MCP servers work, since
    those are read through the home directory rather than the program.
    
    Only when nothing matched the agent anywhere. A state directory is also
    left in the home of every service account an agent sandboxes itself
    into, and those are not installations to list beside your own.
---
 agent/scan_common.go  | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
 agent/scan_darwin.go  |  1 +
 agent/scan_linux.go   |  1 +
 agent/scan_windows.go |  1 +
 4 files changed, 54 insertions(+)

diff --git a/agent/scan_common.go b/agent/scan_common.go
index ed7e483..dd78532 100644
--- a/agent/scan_common.go
+++ b/agent/scan_common.go
@@ -29,6 +29,11 @@ import (
        "github.com/apache/casbin-gateway/internal/hermes"
 )
 
+// InstallMethodConfig marks an agent found by the state directory it left in a
+// home rather than by a program. Nothing can be launched from it, but its
+// configuration is on disk and is what most of Gateway acts on.
+const InstallMethodConfig = "config"
+
 // maxPackageJSONSize limits reads from glob-matched manifests.
 const maxPackageJSONSize = 1024 * 1024
 
@@ -165,6 +170,52 @@ func stampAgentId(installations []Installation, mark int, 
agentId string) {
        }
 }
 
+// scanStateDirs reports an agent that left its state directory in a home while
+// none of the install layouts matched it anywhere: installed by a package
+// manager Gateway does not scan, on a PATH it cannot see, or run through a
+// wrapper. Providers, skills and MCP servers are read and written through the
+// home directory rather than the program, so leaving these out reports "not
+// installed" for an agent someone uses every day.
+//
+// `found` is what this fingerprint matched already, and one hit anywhere is
+// enough to stop: a state directory is also left in the home of every service
+// account an agent sandboxes itself into, and those are not installations
+// anyone wants listed next to their own.
+func scanStateDirs(fingerprint *Fingerprint, homes []homeDir, found 
[]Installation) []Installation {
+       if fingerprint.StateDir == "" || len(found) > 0 {
+               return nil
+       }
+
+       installations := []Installation{}
+       for _, home := range homes {
+               path := filepath.Join(home.path, "."+fingerprint.StateDir)
+               if !hasEntries(path) {
+                       continue
+               }
+               installations = append(installations, Installation{
+                       AgentId:       fingerprint.ID,
+                       Name:          fingerprint.DisplayName,
+                       Path:          path,
+                       InstallMethod: InstallMethodConfig,
+                       Owner:         home.owner,
+               })
+       }
+       return installations
+}
+
+// hasEntries reports a directory that holds something. An empty one is what an
+// uninstall leaves behind, and is not evidence that the agent was ever set up.
+func hasEntries(path string) bool {
+       dir, err := os.Open(path)
+       if err != nil {
+               return false
+       }
+       defer dir.Close()
+
+       names, err := dir.Readdirnames(1)
+       return err == nil && len(names) > 0
+}
+
 // dedupeInstallations returns unique installations ordered by owner and path.
 func dedupeInstallations(installations []Installation) []Installation {
        seen := map[string]bool{}
diff --git a/agent/scan_darwin.go b/agent/scan_darwin.go
index 4b76dab..63236ec 100644
--- a/agent/scan_darwin.go
+++ b/agent/scan_darwin.go
@@ -50,6 +50,7 @@ func scan(ctx context.Context) []Installation {
                        installations = append(installations, 
scanDarwinSystemNpm(ctx, fingerprint, prefix)...)
                }
                stampAgentId(installations, mark, fingerprint.ID)
+               installations = append(installations, 
scanStateDirs(fingerprint, homes, installations[mark:])...)
                fillMissingVersions(installations, mark, fingerprint)
        }
        for _, home := range homes {
diff --git a/agent/scan_linux.go b/agent/scan_linux.go
index ecb7bbc..08bbb29 100644
--- a/agent/scan_linux.go
+++ b/agent/scan_linux.go
@@ -51,6 +51,7 @@ func scan(ctx context.Context) []Installation {
                installations = append(installations, scanSystemPackages(ctx, 
fingerprint)...)
                installations = append(installations, scanHomebrew(ctx, 
fingerprint)...)
                stampAgentId(installations, mark, fingerprint.ID)
+               installations = append(installations, 
scanStateDirs(fingerprint, homes, installations[mark:])...)
                fillMissingVersions(installations, mark, fingerprint)
        }
        for _, home := range homes {
diff --git a/agent/scan_windows.go b/agent/scan_windows.go
index addf750..436bc88 100644
--- a/agent/scan_windows.go
+++ b/agent/scan_windows.go
@@ -59,6 +59,7 @@ func scan(ctx context.Context) []Installation {
                installations = append(installations, scanMachineWinget(ctx, 
fingerprint)...)
                installations = append(installations, scanMachinePrograms(ctx, 
fingerprint)...)
                stampAgentId(installations, mark, fingerprint.ID)
+               installations = append(installations, 
scanStateDirs(fingerprint, homes, installations[mark:])...)
                fillMissingVersions(installations, mark, fingerprint)
        }
        for _, home := range homes {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to