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]