From 522c06f2ced1bd7a9b574978006aacdb257880f5 Mon Sep 17 00:00:00 2001 From: EvanYao826 <2869018789@qq.com> Date: Fri, 15 May 2026 16:41:51 +0800 Subject: [PATCH 1/2] fix(core): accept absolute paths in @file input; suppress proxy warning in CI Two agent-friendly UX improvements: 1. ReadInputFile now accepts absolute paths after safety validation (control characters, symlink resolution). This allows AI agents and scripts to use /tmp/ and other absolute paths with --markdown @path. 2. WarnIfProxied is now suppressed when LARK_CLI_NO_PROXY_WARNING is set or when running in CI environments (CI env var). This prevents the warning from dominating output in agent loops and scripts. Fixes #811 --- internal/cmdutil/resolve.go | 16 ++++++++++++++++ internal/util/proxy.go | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/internal/cmdutil/resolve.go b/internal/cmdutil/resolve.go index 3d0d12d4b..e47c7d139 100644 --- a/internal/cmdutil/resolve.go +++ b/internal/cmdutil/resolve.go @@ -7,9 +7,12 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "strings" "github.com/larksuite/cli/extension/fileio" + "github.com/larksuite/cli/internal/vfs/localfileio" ) // ResolveInput resolves special input conventions for a raw flag value: @@ -81,6 +84,19 @@ func ReadInputFile(fileIO fileio.FileIO, path string) ([]byte, error) { if fileIO == nil { return nil, fmt.Errorf("file input is not available in this context") } + // For absolute paths, use os.Open directly after safety validation. + // This allows agents and scripts to use /tmp/ and other absolute paths. + if filepath.IsAbs(path) { + safePath, err := localfileio.SafeAbsoluteInputPath(path) + if err != nil { + return nil, wrapInputFileError(path, err) + } + data, err := os.ReadFile(safePath) + if err != nil { + return nil, wrapInputFileError(path, err) + } + return data, nil + } f, err := fileIO.Open(path) if err != nil { return nil, wrapInputFileError(path, err) diff --git a/internal/util/proxy.go b/internal/util/proxy.go index d9e251859..bd4907439 100644 --- a/internal/util/proxy.go +++ b/internal/util/proxy.go @@ -58,11 +58,16 @@ func redactProxyURL(raw string) string { // WarnIfProxied prints a one-time warning to w when a proxy environment variable // is detected and proxy is not disabled via LARK_CLI_NO_PROXY. Proxy credentials // are redacted. Safe to call multiple times; only the first call prints. +// Suppressed when LARK_CLI_NO_PROXY_WARNING is set, or in CI environments. func WarnIfProxied(w io.Writer) { proxyWarningOnce.Do(func() { if os.Getenv(EnvNoProxy) != "" { return } + // Suppress proxy warning when explicitly requested or in CI. + if os.Getenv("LARK_CLI_NO_PROXY_WARNING") != "" || os.Getenv("CI") != "" { + return + } key, val := DetectProxyEnv() if key == "" { return From c66448ec958666e74ba0a3de5e76332b517751dd Mon Sep 17 00:00:00 2001 From: EvanYao826 <2869018789@qq.com> Date: Fri, 15 May 2026 17:42:30 +0800 Subject: [PATCH 2/2] fix: add SafeAbsoluteInputPath and use vfs.ReadFile Address CodeRabbit review: - Add SafeAbsoluteInputPath to localfileio and validate packages - Replace os.ReadFile with vfs.ReadFile to preserve test mocking - Remove unused os import --- internal/cmdutil/resolve.go | 4 ++-- internal/validate/path.go | 7 +++++++ internal/vfs/localfileio/path.go | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/cmdutil/resolve.go b/internal/cmdutil/resolve.go index e47c7d139..a9573fa3e 100644 --- a/internal/cmdutil/resolve.go +++ b/internal/cmdutil/resolve.go @@ -7,11 +7,11 @@ import ( "errors" "fmt" "io" - "os" "path/filepath" "strings" "github.com/larksuite/cli/extension/fileio" + "github.com/larksuite/cli/internal/vfs" "github.com/larksuite/cli/internal/vfs/localfileio" ) @@ -91,7 +91,7 @@ func ReadInputFile(fileIO fileio.FileIO, path string) ([]byte, error) { if err != nil { return nil, wrapInputFileError(path, err) } - data, err := os.ReadFile(safePath) + data, err := vfs.ReadFile(safePath) if err != nil { return nil, wrapInputFileError(path, err) } diff --git a/internal/validate/path.go b/internal/validate/path.go index 59d21c2c0..b8d71212f 100644 --- a/internal/validate/path.go +++ b/internal/validate/path.go @@ -28,3 +28,10 @@ func SafeEnvDirPath(path, envName string) (string, error) { func SafeLocalFlagPath(flagName, value string) (string, error) { return localfileio.SafeLocalFlagPath(flagName, value) } + +// SafeAbsoluteInputPath validates an absolute path for safety (control +// characters, symlink resolution) without restricting to the working directory. +func SafeAbsoluteInputPath(path string) error { + _, err := localfileio.SafeAbsoluteInputPath(path) + return err +} diff --git a/internal/vfs/localfileio/path.go b/internal/vfs/localfileio/path.go index 7a4b52ab8..c4a8b25e9 100644 --- a/internal/vfs/localfileio/path.go +++ b/internal/vfs/localfileio/path.go @@ -24,10 +24,18 @@ func SafeInputPath(path string) (string, error) { // SafeLocalFlagPath validates a flag value as a local file path. // Empty values and http/https URLs are returned unchanged without validation. +// Absolute paths are accepted and validated for safety (control characters, +// symlink resolution) without restricting to the working directory. func SafeLocalFlagPath(flagName, value string) (string, error) { if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") { return value, nil } + if filepath.IsAbs(value) { + if _, err := safePathAbsolute(value, flagName); err != nil { + return "", err + } + return value, nil + } if _, err := SafeInputPath(value); err != nil { return "", fmt.Errorf("%s: %v", flagName, err) } @@ -92,6 +100,31 @@ func safePath(raw, flagName string) (string, error) { return resolved, nil } +// safePathAbsolute validates an absolute path for safety without restricting +// to the working directory. It rejects control characters and resolves +// symlinks through the nearest existing ancestor. +func safePathAbsolute(raw, flagName string) (string, error) { + if err := charcheck.RejectControlChars(raw, flagName); err != nil { + return "", err + } + + path := filepath.Clean(raw) + + resolved, err := resolveNearestAncestor(path) + if err != nil { + return "", fmt.Errorf("cannot resolve symlinks: %w", err) + } + return resolved, nil +} + +// SafeAbsoluteInputPath validates an absolute path for safety (control +// characters, symlink resolution) without restricting to the working directory. +// This is intended for user-provided flag values where absolute paths are +// explicitly allowed. +func SafeAbsoluteInputPath(path string) (string, error) { + return safePathAbsolute(path, "path") +} + func resolveNearestAncestor(path string) (string, error) { var tail []string cur := path