Files
larksuite-cli/internal/binding/secret_resolve_file.go
evandance 62ff3d66a6 fix(bind): accept ~/ paths in OpenClaw secret references (#839)
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.
2026-05-13 12:34:43 +08:00

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)
}
}