Files
larksuite__cli/internal/keylessprovider/provider.go
2026-07-22 19:46:33 +08:00

859 lines
28 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
// Package keylessprovider resolves the fixed keyless signer optional dependency
// installed with the OpenClaw Feishu plugin. Application config contains only
// the logical provider ID and keyRef; executable paths and argv are never read
// from application config, environment variables, or PATH.
package keylessprovider
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/keylesshelper"
"github.com/larksuite/cli/internal/vfs"
"golang.org/x/sync/singleflight"
)
const (
ProviderID = core.KeylessProviderLarkSuite
openClawStateDirEnv = "OPENCLAW_STATE_DIR"
openClawHomeEnv = "OPENCLAW_HOME"
openClawConfigEnv = "OPENCLAW_CONFIG_PATH"
openClawDirName = ".openclaw"
pluginID = "openclaw-lark"
pluginPackageName = "@larksuite/openclaw-lark"
signerPackageScope = "@larksuite"
signerBinaryBase = "lark-keyless-signer"
metadataMaxBytes = 64 << 10
binaryMaxBytes = 512 << 20
inspectStdoutLimit = 4 << 20
inspectStderrLimit = 64 << 10
inspectCommandLimit = 30 * time.Second
inspectCacheTTL = 3 * time.Second
)
var semver = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
type platformPackage struct {
name string
npmOS string
npmCPU string
binaryName string
}
type packageManifest struct {
Name string `json:"name"`
Version string `json:"version"`
OS []string `json:"os"`
CPU []string `json:"cpu"`
}
type resolvedProvider struct {
stateDir string
pluginDir string
packageDir string
binaryPath string
packageVersion string
packageSize int64
packageModTimeNS int64
binarySize int64
binaryModTimeNS int64
digest string
}
type inspectDependency struct {
Name string `json:"name"`
Installed bool `json:"installed"`
Optional bool `json:"optional"`
ResolvedPath string `json:"resolvedPath"`
}
type inspectedPlugin struct {
ID string `json:"id"`
Name string `json:"name"`
PackageName string `json:"packageName"`
RootDir string `json:"rootDir"`
Status string `json:"status"`
DependencyStatus struct {
OptionalDependencies []inspectDependency `json:"optionalDependencies"`
} `json:"dependencyStatus"`
}
type inspectUnavailableError struct{ cause error }
func (e *inspectUnavailableError) Error() string { return e.cause.Error() }
func (e *inspectUnavailableError) Unwrap() error { return e.cause }
var (
runOpenClawInspect = executeOpenClawInspect
runOpenClawInspectFresh = executeOpenClawInspectFresh
)
type inspectCacheEntry struct {
data []byte
expiresAt time.Time
}
// inspectResultCache caches only OpenClaw's discovery document. Callers still
// validate the complete directory tree, package metadata, binary mode, and
// binary digest after every lookup, and keylesshelper re-hashes before exec.
// The short TTL removes duplicate OpenClaw startups within one CLI process
// without persisting an executable path across processes or plugin upgrades.
type inspectResultCache struct {
mu sync.Mutex
entries map[string]inspectCacheEntry
flights singleflight.Group
}
var openClawInspectCache inspectResultCache
// Resolve returns a freshly verified command for one provider operation. A
// global manifest avoids restarting OpenClaw when the previously bound package
// is unchanged; the package tree and binary digest are still revalidated.
func Resolve(ctx context.Context, provider string) (*keylesshelper.Command, error) {
command, resolved, err := resolve(ctx, provider, false)
if err != nil {
return nil, err
}
if resolved != nil {
// The manifest is a rebuildable performance index, not an
// authentication source. Runtime resolution remains available if its
// best-effort cache write fails.
_ = saveProviderManifest(*resolved, runtime.GOOS, runtime.GOARCH)
}
return command, nil
}
// PrepareRefresh bypasses both the global manifest and the short-lived
// in-process OpenClaw inspection cache. It returns a freshly verified command
// and a deferred manifest commit. Callers that validate the signer before
// changing binding state must invoke commit only after that validation
// succeeds; abandoning the callback leaves the existing manifest unchanged.
func PrepareRefresh(ctx context.Context, provider string) (*keylesshelper.Command, func() error, error) {
command, resolved, err := resolve(ctx, provider, true)
if err != nil {
return nil, nil, err
}
if resolved == nil {
return command, func() error { return nil }, nil
}
candidate := *resolved
var once sync.Once
var commitErr error
commit := func() error {
once.Do(func() {
commitErr = revalidatePreparedProvider(candidate, runtime.GOOS, runtime.GOARCH)
if commitErr != nil {
return
}
commitErr = saveProviderManifest(candidate, runtime.GOOS, runtime.GOARCH)
})
return commitErr
}
return command, commit, nil
}
// revalidatePreparedProvider closes the gap between signer validation and a
// deferred manifest commit. It repeats the complete package validation and
// requires every captured property to remain identical before the candidate is
// allowed to replace the current manifest entry.
func revalidatePreparedProvider(candidate resolvedProvider, goos, goarch string) error {
spec, err := signerPackageFor(goos, goarch)
if err != nil {
return err
}
verified, err := resolvePackage(candidate.stateDir, candidate.pluginDir, candidate.packageDir, spec, goos)
if err != nil {
return fmt.Errorf("revalidate prepared keyless signer: %w", err)
}
if verified != candidate {
return fmt.Errorf("prepared keyless signer changed before manifest commit")
}
return nil
}
// Refresh bypasses the global manifest and both discovers and records the
// signer reported by the current OpenClaw installation. Transactional bind
// flows should use PrepareRefresh so a failed signer validation cannot replace
// a previously working manifest entry.
func Refresh(ctx context.Context, provider string) (*keylesshelper.Command, error) {
command, commit, err := PrepareRefresh(ctx, provider)
if err != nil {
return nil, err
}
if commit != nil {
if err := commit(); err != nil {
return nil, fmt.Errorf("persist refreshed keyless signer manifest: %w", err)
}
}
return command, nil
}
// resolve returns a non-nil resolvedProvider only for a newly discovered
// package. A manifest hit has already been persisted and therefore returns a
// nil resolvedProvider.
func resolve(ctx context.Context, provider string, forceRefresh bool) (*keylesshelper.Command, *resolvedProvider, error) {
if ctx == nil {
ctx = context.Background()
}
provider = strings.TrimSpace(provider)
if provider == "" {
return nil, nil, nil
}
if provider != ProviderID {
return nil, nil, fmt.Errorf("unknown keyless signer provider %q", provider)
}
stateDir, err := openClawStateDir()
if err != nil {
return nil, nil, fmt.Errorf("resolve OpenClaw state directory: %w", err)
}
if !forceRefresh {
resolved, manifestHit := resolveFromProviderManifest(stateDir, runtime.GOOS, runtime.GOARCH)
if manifestHit {
command, err := keylesshelper.NewProviderCommand(resolved.binaryPath, resolved.packageDir, "", resolved.digest)
return command, nil, err
}
}
inspectResolver := resolveFromInspect
if forceRefresh {
inspectResolver = resolveFromInspectFresh
}
resolved, inspectErr := inspectResolver(ctx, stateDir, runtime.GOOS, runtime.GOARCH)
if inspectErr != nil {
if err := ctx.Err(); err != nil {
return nil, nil, fmt.Errorf("%s provider inspection canceled: %w", ProviderID, err)
}
var unavailable *inspectUnavailableError
if !errors.As(inspectErr, &unavailable) {
return nil, nil, fmt.Errorf("%s provider is unavailable: %w", ProviderID, inspectErr)
}
// Older/local OpenClaw installations may not expose `plugins inspect`.
// The only fallback is the fixed extension-local package path; managed
// npm project directories are never scanned or guessed.
resolved, err = resolveFromStateDir(stateDir, runtime.GOOS, runtime.GOARCH)
if err != nil {
return nil, nil, fmt.Errorf("%s provider is unavailable: OpenClaw inspect failed (%w); fixed extension fallback failed: %w", ProviderID, inspectErr, err)
}
}
// An empty home override deliberately preserves the recipient user's normal
// HOME. The helper still supplies a minimal environment and never inherits
// PATH, proxy, language-runtime, or loader injection variables.
command, err := keylesshelper.NewProviderCommand(resolved.binaryPath, resolved.packageDir, "", resolved.digest)
if err != nil {
return nil, nil, err
}
return command, &resolved, nil
}
func resolveFromStateDir(stateDir, goos, goarch string) (resolvedProvider, error) {
spec, err := signerPackageFor(goos, goarch)
if err != nil {
return resolvedProvider{}, err
}
stateDir, err = cleanAbsolutePath(stateDir)
if err != nil {
return resolvedProvider{}, fmt.Errorf("invalid OpenClaw state directory: %w", err)
}
pluginDir := filepath.Join(stateDir, "extensions", pluginID)
packageDir := signerPackageUnder(filepath.Join(pluginDir, "node_modules"), spec)
return resolvePackage(stateDir, pluginDir, packageDir, spec, goos)
}
func resolveFromInspect(ctx context.Context, stateDir, goos, goarch string) (resolvedProvider, error) {
return resolveFromInspectUsing(ctx, stateDir, goos, goarch, runOpenClawInspect)
}
func resolveFromInspectFresh(ctx context.Context, stateDir, goos, goarch string) (resolvedProvider, error) {
return resolveFromInspectUsing(ctx, stateDir, goos, goarch, runOpenClawInspectFresh)
}
func resolveFromInspectUsing(
ctx context.Context,
stateDir, goos, goarch string,
inspect func(context.Context, string) ([]byte, error),
) (resolvedProvider, error) {
spec, err := signerPackageFor(goos, goarch)
if err != nil {
return resolvedProvider{}, err
}
stateDir, err = cleanAbsolutePath(stateDir)
if err != nil {
return resolvedProvider{}, fmt.Errorf("invalid OpenClaw state directory: %w", err)
}
data, err := inspect(ctx, stateDir)
if err != nil {
return resolvedProvider{}, err
}
plugin, err := decodeInspectedPlugin(data)
if err != nil {
return resolvedProvider{}, fmt.Errorf("parse OpenClaw plugin inspection: %w", err)
}
if plugin.ID != pluginID {
return resolvedProvider{}, fmt.Errorf("OpenClaw inspected unexpected plugin %q", plugin.ID)
}
if plugin.PackageName != "" && plugin.PackageName != pluginPackageName {
return resolvedProvider{}, fmt.Errorf("OpenClaw plugin package %q does not match %q", plugin.PackageName, pluginPackageName)
}
if plugin.Name != "" && plugin.Name != pluginPackageName && plugin.Name != "Feishu" {
return resolvedProvider{}, fmt.Errorf("OpenClaw plugin name %q is not recognized", plugin.Name)
}
if plugin.Status != "" && plugin.Status != "loaded" {
return resolvedProvider{}, fmt.Errorf("OpenClaw plugin is not loaded (status %q)", plugin.Status)
}
pluginDir, err := cleanAbsolutePath(plugin.RootDir)
if err != nil {
return resolvedProvider{}, fmt.Errorf("invalid inspected plugin root: %w", err)
}
if err := ensureWithin(stateDir, pluginDir); err != nil {
return resolvedProvider{}, fmt.Errorf("inspected plugin root is outside OpenClaw state: %w", err)
}
var dependency *inspectDependency
for i := range plugin.DependencyStatus.OptionalDependencies {
candidate := &plugin.DependencyStatus.OptionalDependencies[i]
if candidate.Name != spec.name {
continue
}
if dependency != nil {
return resolvedProvider{}, fmt.Errorf("OpenClaw inspection contains duplicate signer dependency %q", spec.name)
}
dependency = candidate
}
if dependency == nil || !dependency.Optional || !dependency.Installed || strings.TrimSpace(dependency.ResolvedPath) == "" {
return resolvedProvider{}, fmt.Errorf("OpenClaw plugin optional signer dependency %q is not installed", spec.name)
}
packageDir, err := cleanAbsolutePath(dependency.ResolvedPath)
if err != nil {
return resolvedProvider{}, fmt.Errorf("invalid inspected signer package path: %w", err)
}
if err := ensureWithin(stateDir, packageDir); err != nil {
return resolvedProvider{}, fmt.Errorf("inspected signer package is outside OpenClaw state: %w", err)
}
if !allowedPackageLocation(pluginDir, packageDir, spec, goos) {
return resolvedProvider{}, fmt.Errorf("inspected signer package is neither plugin-local nor managed-project-hoisted")
}
return resolvePackage(stateDir, pluginDir, packageDir, spec, goos)
}
func resolvePackage(stateDir, pluginDir, packageDir string, spec platformPackage, goos string) (resolvedProvider, error) {
packageJSON := filepath.Join(packageDir, "package.json")
binDir := filepath.Join(packageDir, "bin")
binaryPath := filepath.Join(binDir, spec.binaryName)
for _, dir := range []string{pluginDir, packageDir, binDir} {
if err := validateDirectoryTree(stateDir, dir); err != nil {
return resolvedProvider{}, err
}
}
for _, file := range []string{packageJSON, binaryPath} {
if err := ensureWithin(stateDir, file); err != nil {
return resolvedProvider{}, err
}
if err := validateProviderObject(file, false); err != nil {
return resolvedProvider{}, err
}
}
data, err := readMetadata(packageJSON)
if err != nil {
return resolvedProvider{}, fmt.Errorf("read signer package metadata: %w", err)
}
var manifest packageManifest
if err := decodeJSONObject(data, &manifest); err != nil {
return resolvedProvider{}, fmt.Errorf("parse signer package metadata: %w", err)
}
if manifest.Name != spec.name {
return resolvedProvider{}, fmt.Errorf("signer package name %q does not match %q", manifest.Name, spec.name)
}
if !semver.MatchString(manifest.Version) {
return resolvedProvider{}, fmt.Errorf("signer package version %q is not valid semver", manifest.Version)
}
if len(manifest.OS) != 1 || manifest.OS[0] != spec.npmOS {
return resolvedProvider{}, fmt.Errorf("signer package os metadata does not match %s", spec.npmOS)
}
if len(manifest.CPU) != 1 || manifest.CPU[0] != spec.npmCPU {
return resolvedProvider{}, fmt.Errorf("signer package cpu metadata does not match %s", spec.npmCPU)
}
packageInfo, err := vfs.Lstat(packageJSON)
if err != nil {
return resolvedProvider{}, fmt.Errorf("stat signer package metadata: %w", err)
}
info, err := vfs.Lstat(binaryPath)
if err != nil {
return resolvedProvider{}, fmt.Errorf("stat signer binary: %w", err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return resolvedProvider{}, fmt.Errorf("signer binary is not a regular file")
}
if goos != "windows" && info.Mode().Perm()&0o111 == 0 {
return resolvedProvider{}, fmt.Errorf("signer binary is not executable")
}
digest, err := hashRegularFile(binaryPath, binaryMaxBytes)
if err != nil {
return resolvedProvider{}, fmt.Errorf("hash signer binary: %w", err)
}
return resolvedProvider{
stateDir: stateDir,
pluginDir: pluginDir,
packageDir: packageDir,
binaryPath: binaryPath,
packageVersion: manifest.Version,
packageSize: packageInfo.Size(),
packageModTimeNS: packageInfo.ModTime().UnixNano(),
binarySize: info.Size(),
binaryModTimeNS: info.ModTime().UnixNano(),
digest: digest,
}, nil
}
func signerPackageFor(goos, goarch string) (platformPackage, error) {
key := goos + "/" + goarch
packages := map[string]platformPackage{
"darwin/arm64": {"@larksuite/lark-keyless-signer-darwin-arm64", "darwin", "arm64", signerBinaryBase},
"darwin/amd64": {"@larksuite/lark-keyless-signer-darwin-x64", "darwin", "x64", signerBinaryBase},
"linux/arm64": {"@larksuite/lark-keyless-signer-linux-arm64", "linux", "arm64", signerBinaryBase},
"linux/amd64": {"@larksuite/lark-keyless-signer-linux-x64", "linux", "x64", signerBinaryBase},
"windows/amd64": {"@larksuite/lark-keyless-signer-win32-x64", "win32", "x64", signerBinaryBase + ".exe"},
}
if spec, ok := packages[key]; ok {
return spec, nil
}
return platformPackage{}, fmt.Errorf("OpenClaw keyless signer optional package is unsupported on %s/%s", goos, goarch)
}
func signerPackageUnder(nodeModules string, spec platformPackage) string {
return filepath.Join(nodeModules, signerPackageScope, strings.TrimPrefix(spec.name, signerPackageScope+"/"))
}
func allowedPackageLocation(pluginDir, packageDir string, spec platformPackage, goos string) bool {
pluginLocal := signerPackageUnder(filepath.Join(pluginDir, "node_modules"), spec)
if samePath(pluginLocal, packageDir, goos) {
return true
}
// npm-pack managed projects may hoist the optional dependency beside the
// plugin package:
// <project>/node_modules/@larksuite/openclaw-lark
// <project>/node_modules/@larksuite/lark-keyless-signer-...
pluginNodeModules := filepath.Dir(filepath.Dir(pluginDir))
expectedPlugin := filepath.Join(pluginNodeModules, signerPackageScope, strings.TrimPrefix(pluginPackageName, signerPackageScope+"/"))
if filepath.Base(pluginNodeModules) != "node_modules" || !samePath(expectedPlugin, pluginDir, goos) {
return false
}
return samePath(signerPackageUnder(pluginNodeModules, spec), packageDir, goos)
}
func samePath(left, right, goos string) bool {
left, right = filepath.Clean(left), filepath.Clean(right)
if goos == "windows" {
return strings.EqualFold(left, right)
}
return left == right
}
func decodeInspectedPlugin(data []byte) (inspectedPlugin, error) {
if len(data) == 0 || len(data) > inspectStdoutLimit {
return inspectedPlugin{}, fmt.Errorf("inspection output size is invalid")
}
var envelope struct {
Plugin json.RawMessage `json:"plugin"`
}
if err := decodeJSONObject(data, &envelope); err != nil {
return inspectedPlugin{}, err
}
var plugin inspectedPlugin
if len(envelope.Plugin) != 0 && string(envelope.Plugin) != "null" {
if err := decodeJSONObject(envelope.Plugin, &plugin); err != nil {
return inspectedPlugin{}, err
}
} else if err := decodeJSONObject(data, &plugin); err != nil {
return inspectedPlugin{}, err
}
return plugin, nil
}
func executeOpenClawInspect(ctx context.Context, stateDir string) ([]byte, error) {
if ctx == nil {
ctx = context.Background()
}
executable, cacheKey, err := prepareOpenClawInspect(stateDir)
if err != nil {
return nil, err
}
return openClawInspectCache.load(ctx, cacheKey, func(loadCtx context.Context) ([]byte, error) {
return executeOpenClawInspectCommand(loadCtx, stateDir, executable)
})
}
// executeOpenClawInspectFresh deliberately skips inspectResultCache, including
// its in-flight singleflight results. Bind/repair discovery must observe the
// current plugin generation even if normal resolution inspected the same
// OpenClaw installation during the preceding few seconds.
func executeOpenClawInspectFresh(ctx context.Context, stateDir string) ([]byte, error) {
if ctx == nil {
ctx = context.Background()
}
executable, _, err := prepareOpenClawInspect(stateDir)
if err != nil {
return nil, err
}
return executeOpenClawInspectCommand(ctx, stateDir, executable)
}
func prepareOpenClawInspect(stateDir string) (string, string, error) {
executable, err := exec.LookPath("openclaw")
if err != nil {
return "", "", &inspectUnavailableError{cause: fmt.Errorf("openclaw executable not found: %w", err)}
}
if !filepath.IsAbs(executable) {
cwd, cwdErr := vfs.Getwd()
if cwdErr != nil {
return "", "", fmt.Errorf("resolve current directory for openclaw executable: %w", cwdErr)
}
executable = filepath.Join(cwd, executable)
}
executable, err = vfs.EvalSymlinks(executable)
if err != nil {
return "", "", fmt.Errorf("resolve openclaw executable symlinks: %w", err)
}
executable, err = cleanAbsolutePath(executable)
if err != nil {
return "", "", fmt.Errorf("validate openclaw executable path: %w", err)
}
if err := validateInspectExecutable(executable); err != nil {
return "", "", fmt.Errorf("validate openclaw executable: %w", err)
}
info, err := vfs.Lstat(executable)
if err != nil || runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 {
return "", "", fmt.Errorf("openclaw executable is not executable")
}
cacheKey := strings.Join([]string{
stateDir,
executable,
fmt.Sprintf("%d", info.Size()),
fmt.Sprintf("%d", info.ModTime().UnixNano()),
strings.TrimSpace(os.Getenv(openClawConfigEnv)),
}, "\x00")
return executable, cacheKey, nil
}
func (c *inspectResultCache) load(ctx context.Context, key string, loader func(context.Context) ([]byte, error)) ([]byte, error) {
now := time.Now()
c.mu.Lock()
if entry, ok := c.entries[key]; ok && now.Before(entry.expiresAt) {
data := append([]byte(nil), entry.data...)
c.mu.Unlock()
return data, nil
}
c.mu.Unlock()
resultCh := c.flights.DoChan(key, func() (any, error) {
now := time.Now()
c.mu.Lock()
if entry, ok := c.entries[key]; ok && now.Before(entry.expiresAt) {
data := append([]byte(nil), entry.data...)
c.mu.Unlock()
return data, nil
}
c.mu.Unlock()
data, err := loader(ctx)
if err != nil {
return nil, err
}
data = append([]byte(nil), data...)
c.mu.Lock()
if c.entries == nil {
c.entries = make(map[string]inspectCacheEntry)
}
c.entries[key] = inspectCacheEntry{data: data, expiresAt: time.Now().Add(inspectCacheTTL)}
c.mu.Unlock()
return append([]byte(nil), data...), nil
})
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-resultCh:
if result.Err != nil {
return nil, result.Err
}
data, ok := result.Val.([]byte)
if !ok {
return nil, fmt.Errorf("OpenClaw inspect cache returned an invalid result")
}
return append([]byte(nil), data...), nil
}
}
func executeOpenClawInspectCommand(ctx context.Context, stateDir, executable string) ([]byte, error) {
inspectCtx, cancel := context.WithTimeout(ctx, inspectCommandLimit)
defer cancel()
cmd, err := newOpenClawInspectCommand(inspectCtx, executable, openClawInspectEnvironment(stateDir))
if err != nil {
return nil, &inspectUnavailableError{cause: fmt.Errorf("prepare openclaw plugin inspection: %w", err)}
}
stdout := &cappedBuffer{limit: inspectStdoutLimit}
stderr := &cappedBuffer{limit: inspectStderrLimit}
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
if inspectCtx.Err() != nil {
return nil, &inspectUnavailableError{cause: fmt.Errorf("openclaw plugin inspection stopped: %w", inspectCtx.Err())}
}
return nil, &inspectUnavailableError{cause: fmt.Errorf("openclaw plugin inspection failed: %w (stderr omitted)", err)}
}
if stdout.exceeded {
return nil, fmt.Errorf("openclaw plugin inspection stdout exceeds %d bytes", inspectStdoutLimit)
}
if stderr.exceeded {
return nil, fmt.Errorf("openclaw plugin inspection stderr exceeds %d bytes", inspectStderrLimit)
}
return append([]byte(nil), stdout.Bytes()...), nil
}
func openClawInspectEnvironment(stateDir string) []string {
keep := map[string]bool{
"HOME": true, "PATH": true, "PATHEXT": true, "TMPDIR": true, "TEMP": true, "TMP": true,
"LANG": true, "LC_ALL": true, "USERPROFILE": true, "SYSTEMROOT": true, "WINDIR": true,
"COMSPEC": true, "APPDATA": true, "LOCALAPPDATA": true,
"OPENCLAW_HOME": true, openClawConfigEnv: true,
}
env := make([]string, 0, len(keep)+1)
for _, entry := range os.Environ() {
name := entry
if idx := strings.IndexByte(entry, '='); idx >= 0 {
name = entry[:idx]
}
if keep[strings.ToUpper(name)] {
env = append(env, entry)
}
}
return append(env, openClawStateDirEnv+"="+stateDir)
}
type cappedBuffer struct {
bytes.Buffer
limit int
exceeded bool
}
func (b *cappedBuffer) Write(data []byte) (int, error) {
originalLen := len(data)
remaining := b.limit - b.Len()
if remaining < len(data) {
b.exceeded = true
if remaining < 0 {
remaining = 0
}
data = data[:remaining]
}
_, _ = b.Buffer.Write(data)
return originalLen, nil
}
func validateDirectoryTree(root, target string) error {
root, err := cleanAbsolutePath(root)
if err != nil {
return err
}
target, err = cleanAbsolutePath(target)
if err != nil {
return err
}
if err := ensureWithin(root, target); err != nil {
return err
}
if err := validateProviderObject(root, true); err != nil {
return err
}
rel, err := filepath.Rel(root, target)
if err != nil {
return err
}
current := root
if rel == "." {
return nil
}
for _, part := range strings.Split(rel, string(filepath.Separator)) {
if part == "" || part == "." || part == ".." {
return fmt.Errorf("provider directory tree contains invalid component %q", part)
}
current = filepath.Join(current, part)
if err := validateProviderObject(current, true); err != nil {
return err
}
}
return nil
}
func openClawStateDir() (string, error) {
if stateDir := strings.TrimSpace(os.Getenv(openClawStateDirEnv)); stateDir != "" {
return resolveOpenClawPath(stateDir)
}
if configPath := strings.TrimSpace(os.Getenv(openClawConfigEnv)); configPath != "" {
resolved, err := resolveOpenClawPath(configPath)
if err != nil {
return "", fmt.Errorf("invalid %s: %w", openClawConfigEnv, err)
}
return cleanAbsolutePath(filepath.Dir(resolved))
}
home, err := openClawEffectiveHome()
if err != nil {
return "", err
}
return cleanAbsolutePath(filepath.Join(home, openClawDirName))
}
func resolveOpenClawPath(path string) (string, error) {
home, err := openClawEffectiveHome()
if err != nil {
return "", err
}
return expandWithHomeAndClean(path, home)
}
func openClawEffectiveHome() (string, error) {
osHome, err := vfs.UserHomeDir()
if err != nil || strings.TrimSpace(osHome) == "" {
return "", fmt.Errorf("resolve user home: %w", err)
}
osHome, err = cleanAbsolutePath(filepath.Clean(osHome))
if err != nil {
return "", fmt.Errorf("invalid user home: %w", err)
}
if configured := strings.TrimSpace(os.Getenv(openClawHomeEnv)); configured != "" {
home, err := expandWithHomeAndClean(configured, osHome)
if err != nil {
return "", fmt.Errorf("invalid %s: %w", openClawHomeEnv, err)
}
return home, nil
}
return osHome, nil
}
func expandWithHomeAndClean(path, home string) (string, error) {
if path == "~" || strings.HasPrefix(path, "~/") || strings.HasPrefix(path, `~\`) {
if path == "~" {
path = home
} else {
path = filepath.Join(home, path[2:])
}
} else if strings.HasPrefix(path, "~") {
return "", fmt.Errorf("named-user home expansion is not supported")
}
return cleanAbsolutePath(path)
}
func cleanAbsolutePath(path string) (string, error) {
if strings.IndexByte(path, 0) >= 0 {
return "", fmt.Errorf("path contains NUL")
}
if !filepath.IsAbs(path) {
return "", fmt.Errorf("path must be absolute")
}
clean := filepath.Clean(path)
if clean != path {
return "", fmt.Errorf("path must be clean (got %q)", path)
}
return clean, nil
}
func ensureWithin(root, path string) error {
rel, err := filepath.Rel(root, path)
if err != nil {
return fmt.Errorf("check signer path boundary: %w", err)
}
if rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("signer path escapes OpenClaw state directory")
}
return nil
}
func decodeJSONObject(data []byte, out any) error {
trimmed := strings.TrimSpace(string(data))
if !strings.HasPrefix(trimmed, "{") || !strings.HasSuffix(trimmed, "}") {
return fmt.Errorf("expected JSON object")
}
decoder := json.NewDecoder(strings.NewReader(trimmed))
if err := decoder.Decode(out); err != nil {
return err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return fmt.Errorf("multiple JSON values are not allowed")
}
return err
}
return nil
}
func readMetadata(path string) ([]byte, error) {
info, err := vfs.Lstat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() < 2 || info.Size() > metadataMaxBytes {
return nil, fmt.Errorf("metadata size/type is invalid")
}
data, err := vfs.ReadFile(path)
if err != nil {
return nil, err
}
if len(data) > metadataMaxBytes {
return nil, fmt.Errorf("metadata exceeds %d bytes", metadataMaxBytes)
}
return data, nil
}
func hashRegularFile(path string, maxSize int64) (string, error) {
info, err := vfs.Lstat(path)
if err != nil {
return "", err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() <= 0 || info.Size() > maxSize {
return "", fmt.Errorf("file size/type is invalid")
}
f, err := vfs.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
n, err := io.Copy(h, io.LimitReader(f, maxSize+1))
if err != nil {
return "", err
}
if n != info.Size() || n > maxSize {
return "", fmt.Errorf("file changed while hashing")
}
return hex.EncodeToString(h.Sum(nil)), nil
}