mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
OpenClaw stores secret file paths in user-authored ~/-relative form so the configuration stays portable across machines. lark-cli config bind previously rejected these as non-absolute, blocking users until they rewrote the OpenClaw config with literal absolute paths. bind now resolves ~ to the OpenClaw home directory (OPENCLAW_HOME if set, otherwise the OS home) before the path audit runs, mirroring how OpenClaw itself reads the same field. Cwd-relative paths and other unsafe locations are still rejected as before.
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package binding
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
// SingleValueFileRefID is the required ref.ID for singleValue file mode
|
|
// (aligned with OpenClaw ref-contract.ts SINGLE_VALUE_FILE_REF_ID).
|
|
const SingleValueFileRefID = "$SINGLE_VALUE"
|
|
|
|
// resolveFileRef handles {source:"file"} SecretRef resolution.
|
|
// Reads the file via assertSecurePath audit, then extracts the secret value
|
|
// based on the provider's mode (singleValue or json with JSON Pointer).
|
|
func resolveFileRef(ref *SecretRef, pc *ProviderConfig) (string, error) {
|
|
if pc.Path == "" {
|
|
return "", fmt.Errorf("file provider path is empty")
|
|
}
|
|
|
|
// OpenClaw preserves user-authored `~/...` paths verbatim on disk for
|
|
// portability and resolves them at read time. lark-cli reads the file
|
|
// raw, so we mirror that resolution here before the audit — otherwise
|
|
// an unambiguous home-relative path would be rejected by
|
|
// requireAbsolutePath, which is meant to guard against cwd-relative
|
|
// paths (a different concern). expandTildePath honours OPENCLAW_HOME so
|
|
// a tilde inside an OPENCLAW_HOME-overridden config resolves to the
|
|
// same absolute path OpenClaw itself would have used.
|
|
targetPath := expandTildePath(pc.Path)
|
|
|
|
// Security audit on file path
|
|
securePath, err := AssertSecurePath(AuditParams{
|
|
TargetPath: targetPath,
|
|
Label: "secrets.providers file path",
|
|
TrustedDirs: pc.TrustedDirs,
|
|
AllowInsecurePath: pc.AllowInsecurePath,
|
|
AllowReadableByOthers: false, // file provider: strict by default
|
|
AllowSymlinkPath: false,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("file provider security audit failed: %w", err)
|
|
}
|
|
|
|
// Read file content
|
|
maxBytes := pc.MaxBytes
|
|
if maxBytes <= 0 {
|
|
maxBytes = DefaultFileMaxBytes
|
|
}
|
|
|
|
// Note: vfs.ReadFile loads the entire file. maxBytes is enforced post-read
|
|
// because vfs does not expose a size-limited reader. For secret files this
|
|
// is acceptable (default limit 1 MiB; secrets are typically < 1 KB).
|
|
data, err := vfs.ReadFile(securePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read secret file %s: %w", securePath, err)
|
|
}
|
|
|
|
if len(data) > maxBytes {
|
|
return "", fmt.Errorf("file provider exceeded maxBytes (%d)", maxBytes)
|
|
}
|
|
|
|
content := string(data)
|
|
mode := pc.Mode
|
|
if mode == "" {
|
|
mode = "json" // default mode per OpenClaw
|
|
}
|
|
|
|
switch mode {
|
|
case "singleValue":
|
|
// OpenClaw requires ref.id == SINGLE_VALUE_FILE_REF_ID for singleValue mode
|
|
if ref.ID != SingleValueFileRefID {
|
|
return "", fmt.Errorf("singleValue file provider expects ref id %q, got %q",
|
|
SingleValueFileRefID, ref.ID)
|
|
}
|
|
// Entire file content is the secret; trim trailing newline
|
|
return strings.TrimRight(content, "\r\n"), nil
|
|
|
|
case "json":
|
|
// Parse as JSON, then navigate via JSON Pointer (ref.ID)
|
|
var parsed interface{}
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
return "", fmt.Errorf("file provider JSON parse error: %w", err)
|
|
}
|
|
|
|
value, err := ReadJSONPointer(parsed, ref.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("file provider JSON Pointer %q: %w", ref.ID, err)
|
|
}
|
|
|
|
// Value must be a string
|
|
strValue, ok := value.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("file provider JSON Pointer %q resolved to non-string value", ref.ID)
|
|
}
|
|
return strValue, nil
|
|
|
|
default:
|
|
return "", fmt.Errorf("unsupported file provider mode %q", mode)
|
|
}
|
|
}
|