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


The following commit(s) were added to refs/heads/master by this push:
     new 236768b  fix(agent): read a version from a config-only install
236768b is described below

commit 236768b1a92f66ac68c7114abd351f0af75120b4
Author: Yang Luo <[email protected]>
AuthorDate: Thu Sep 3 23:29:27 2026 +0800

    fix(agent): read a version from a config-only install
---
 agent/fingerprint.go                | 36 ++++++++-------
 agent/fingerprint_load.go           |  4 ++
 agent/fingerprints/claude-code.json |  8 +++-
 agent/version.go                    | 92 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 121 insertions(+), 19 deletions(-)

diff --git a/agent/fingerprint.go b/agent/fingerprint.go
index a10bd02..dbfca0d 100644
--- a/agent/fingerprint.go
+++ b/agent/fingerprint.go
@@ -47,20 +47,24 @@ type Fingerprint struct {
        // is how a browser hands a finished sign-in back to it.
        LinkScheme string `json:"linkScheme,omitempty"`
 
-       StateDir            string              `json:"stateDir,omitempty"`
-       NpmPackage          string              `json:"npmPackage,omitempty"`
-       ExtraUnixNpmDirs    []string            
`json:"extraUnixNpmDirs,omitempty"`
-       ExtraWindowsNpmDirs []string            
`json:"extraWindowsNpmDirs,omitempty"`
-       WingetPackage       string              `json:"wingetPackage,omitempty"`
-       MSIXFamily          string              `json:"msixFamily,omitempty"`
-       DesktopInstallerDir string              
`json:"desktopInstallerDir,omitempty"`
-       WindowsProgramDirs  []string            
`json:"windowsProgramDirs,omitempty"`
-       WindowsUserDirs     []string            
`json:"windowsUserDirs,omitempty"`
-       HomeDirs            []string            `json:"homeDirs,omitempty"`
-       HomebrewCasks       []string            `json:"homebrewCasks,omitempty"`
-       SystemPackage       string              `json:"systemPackage,omitempty"`
-       BuildInfoModule     string              
`json:"buildInfoModule,omitempty"`
-       BuildInfoVersionVar string              
`json:"buildInfoVersionVar,omitempty"`
-       VersionFile         string              `json:"versionFile,omitempty"`
-       LocalServer         *localserver.Server `json:"localServer,omitempty"`
+       StateDir            string   `json:"stateDir,omitempty"`
+       NpmPackage          string   `json:"npmPackage,omitempty"`
+       ExtraUnixNpmDirs    []string `json:"extraUnixNpmDirs,omitempty"`
+       ExtraWindowsNpmDirs []string `json:"extraWindowsNpmDirs,omitempty"`
+       WingetPackage       string   `json:"wingetPackage,omitempty"`
+       MSIXFamily          string   `json:"msixFamily,omitempty"`
+       DesktopInstallerDir string   `json:"desktopInstallerDir,omitempty"`
+       WindowsProgramDirs  []string `json:"windowsProgramDirs,omitempty"`
+       WindowsUserDirs     []string `json:"windowsUserDirs,omitempty"`
+       HomeDirs            []string `json:"homeDirs,omitempty"`
+       HomebrewCasks       []string `json:"homebrewCasks,omitempty"`
+       SystemPackage       string   `json:"systemPackage,omitempty"`
+       BuildInfoModule     string   `json:"buildInfoModule,omitempty"`
+       BuildInfoVersionVar string   `json:"buildInfoVersionVar,omitempty"`
+       VersionFile         string   `json:"versionFile,omitempty"`
+       // StateVersionGlob matches JSONL files under the state directory whose
+       // records carry the version of the agent that wrote them. It is the 
only
+       // version an installation found by its configuration alone can report.
+       StateVersionGlob string              `json:"stateVersionGlob,omitempty"`
+       LocalServer      *localserver.Server `json:"localServer,omitempty"`
 }
diff --git a/agent/fingerprint_load.go b/agent/fingerprint_load.go
index dae4701..97fce18 100644
--- a/agent/fingerprint_load.go
+++ b/agent/fingerprint_load.go
@@ -108,6 +108,10 @@ func validateFingerprint(f Fingerprint, name string) error 
{
                return fmt.Errorf("installUrl %q is not an http(s) URL", 
f.InstallUrl)
        }
 
+       if f.StateVersionGlob != "" && f.StateDir == "" {
+               return errors.New("stateVersionGlob has no stateDir to resolve 
against")
+       }
+
        if err := validateLocalServer(f.LocalServer); err != nil {
                return err
        }
diff --git a/agent/fingerprints/claude-code.json 
b/agent/fingerprints/claude-code.json
index f73bf89..465fb4f 100644
--- a/agent/fingerprints/claude-code.json
+++ b/agent/fingerprints/claude-code.json
@@ -7,6 +7,10 @@
   "stateDir": "claude",
   "npmPackage": "@anthropic-ai/claude-code",
   "wingetPackage": "Anthropic.ClaudeCode",
-  "homebrewCasks": ["claude-code", "claude-code@latest"],
-  "systemPackage": "claude-code"
+  "homebrewCasks": [
+    "claude-code",
+    "claude-code@latest"
+  ],
+  "systemPackage": "claude-code",
+  "stateVersionGlob": "projects/*/*.jsonl"
 }
diff --git a/agent/version.go b/agent/version.go
index ac07937..fa8aa33 100644
--- a/agent/version.go
+++ b/agent/version.go
@@ -17,15 +17,26 @@ package agent
 import (
        "bytes"
        "debug/buildinfo"
+       "encoding/json"
+       "io"
        "os"
        "path/filepath"
        "regexp"
+       "sort"
        "strings"
 )
 
 // maxVersionFileSize limits reads from version files.
 const maxVersionFileSize = 4 * 1024
 
+// maxStateVersionFiles is how many of the newest matches are opened, in case
+// the newest one was cut short before it recorded anything.
+const maxStateVersionFiles = 3
+
+// maxStateVersionHead is how much of a record file's start is read. The 
version
+// sits on the first records, and a later one can be megabytes of tool output.
+const maxStateVersionHead = 64 * 1024
+
 var pseudoVersion = regexp.MustCompile(`\d{14}-[0-9a-f]{12}`)
 
 var describeSuffix = regexp.MustCompile(`-\d+-g[0-9a-f]+$`)
@@ -109,14 +120,93 @@ func executableVersionFile(binaryPath, fileName string) 
string {
        return sanitizeVersion(string(data))
 }
 
+// stateDirVersion reads the version out of the newest records the agent wrote
+// under its state directory. An installation found by its configuration alone
+// has no program to read a version from, but its own records name one.
+func stateDirVersion(stateDir, glob string) string {
+       if stateDir == "" || glob == "" {
+               return ""
+       }
+       matches, err := filepath.Glob(filepath.Join(stateDir, 
filepath.FromSlash(glob)))
+       if err != nil {
+               return ""
+       }
+
+       type candidate struct {
+               path string
+               info os.FileInfo
+       }
+       files := make([]candidate, 0, len(matches))
+       for _, match := range matches {
+               info, err := os.Stat(match)
+               if err != nil || !info.Mode().IsRegular() || info.Size() == 0 {
+                       continue
+               }
+               files = append(files, candidate{path: match, info: info})
+       }
+       sort.SliceStable(files, func(left, right int) bool {
+               return 
files[left].info.ModTime().After(files[right].info.ModTime())
+       })
+       if len(files) > maxStateVersionFiles {
+               files = files[:maxStateVersionFiles]
+       }
+
+       for _, file := range files {
+               if version := recordVersion(file.path); version != "" {
+                       return version
+               }
+       }
+       return ""
+}
+
+// recordVersion is the "version" field of the first JSON record that has one.
+func recordVersion(path string) string {
+       file, err := os.Open(path)
+       if err != nil {
+               return ""
+       }
+       defer file.Close()
+
+       head := make([]byte, maxStateVersionHead)
+       read, err := io.ReadFull(file, head)
+       if read == 0 || (err != nil && err != io.EOF && err != 
io.ErrUnexpectedEOF) {
+               return ""
+       }
+       head = head[:read]
+       // The last line is dropped unless the read stopped on a record 
boundary:
+       // what follows a full buffer is half a record.
+       if end := bytes.LastIndexByte(head, '\n'); end >= 0 {
+               head = head[:end]
+       } else if err == nil {
+               return ""
+       }
+
+       for _, line := range bytes.Split(head, []byte("\n")) {
+               var record struct {
+                       Version string `json:"version"`
+               }
+               if json.Unmarshal(bytes.TrimSpace(line), &record) != nil {
+                       continue
+               }
+               if version := sanitizeVersion(record.Version); version != "" {
+                       return version
+               }
+       }
+       return ""
+}
+
 func fillMissingVersions(installations []Installation, mark int, fingerprint 
*Fingerprint) {
-       if fingerprint.BuildInfoModule == "" && fingerprint.VersionFile == "" {
+       if fingerprint.BuildInfoModule == "" && fingerprint.VersionFile == "" 
&& fingerprint.StateVersionGlob == "" {
                return
        }
        for i := mark; i < len(installations); i++ {
                if installations[i].Version != "" {
                        continue
                }
+               if installations[i].InstallMethod == InstallMethodConfig {
+                       installations[i].Version = 
stateDirVersion(installations[i].Path, fingerprint.StateVersionGlob)
+                       continue
+               }
                version := executableBuildVersion(
                        installations[i].Path, fingerprint.BuildInfoModule, 
fingerprint.BuildInfoVersionVar)
                if version == "" {


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

Reply via email to