diff --git a/internal/cmdutil/factory_default.go b/internal/cmdutil/factory_default.go index 514aaf93f..39f8a7b50 100644 --- a/internal/cmdutil/factory_default.go +++ b/internal/cmdutil/factory_default.go @@ -102,7 +102,7 @@ func safeRedirectPolicy(req *http.Request, via []*http.Request) error { func cachedHttpClientFunc(f *Factory) func() (*http.Client, error) { return sync.OnceValues(func() (*http.Client, error) { - transport.WarnIfProxied(f.IOStreams.ErrOut) + transport.WarnIfProxied(f.IOStreams.ErrOut, f.IOStreams.IsTerminal) var rt http.RoundTripper = transport.Shared() rt = &RetryTransport{Base: rt} @@ -129,7 +129,7 @@ func cachedLarkClientFunc(f *Factory) func() (*lark.Client, error) { lark.WithLogLevel(larkcore.LogLevelError), lark.WithHeaders(BaseSecurityHeaders()), } - transport.WarnIfProxied(f.IOStreams.ErrOut) + transport.WarnIfProxied(f.IOStreams.ErrOut, f.IOStreams.IsTerminal) opts = append(opts, lark.WithHttpClient(&http.Client{ Transport: buildSDKTransport(), CheckRedirect: safeRedirectPolicy, diff --git a/internal/transport/warn.go b/internal/transport/warn.go index cac050f72..9cb68688f 100644 --- a/internal/transport/warn.go +++ b/internal/transport/warn.go @@ -73,7 +73,18 @@ 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. -func WarnIfProxied(w io.Writer) { +// +// The warning is suppressed entirely when interactive is false — i.e. stdin is +// not a TTY, which is the case for agent / CI / piped invocations. Those callers +// frequently parse the CLI's stdout as JSON and merge streams with `2>&1`; a +// stray stderr warning then corrupts the parsed payload. Suppressing in the +// non-interactive case keeps machine-consumed output clean, while human +// interactive sessions still get the security notice. Passing interactive=false +// does not consume the once guard, so a later interactive call can still warn. +func WarnIfProxied(w io.Writer, interactive bool) { + if !interactive { + return + } proxyWarningOnce.Do(func() { // Proxy plugin mode overrides env proxies and LARK_CLI_NO_PROXY (see // Shared), so its warning and disable instructions take precedence. diff --git a/internal/transport/warn_test.go b/internal/transport/warn_test.go index 13708ca7e..6f1eb530c 100644 --- a/internal/transport/warn_test.go +++ b/internal/transport/warn_test.go @@ -44,7 +44,7 @@ func TestWarnIfProxied_WithProxy(t *testing.T) { t.Setenv("HTTPS_PROXY", "http://corp-proxy:3128") var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) out := buf.String() if out == "" { @@ -70,13 +70,30 @@ func TestWarnIfProxied_WithoutProxy(t *testing.T) { } var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) if buf.Len() != 0 { t.Errorf("expected no output when no proxy is set, got: %s", buf.String()) } } +func TestWarnIfProxied_SilentWhenNonInteractive(t *testing.T) { + proxyWarningOnce = sync.Once{} + + // Non-interactive (interactive=false) mirrors agent / CI / piped invocations + // where stdin is not a TTY. The proxy warning must be suppressed so callers + // that parse stdout as JSON — often merging streams with `2>&1` — are not + // corrupted by a stray stderr line. + t.Setenv("HTTPS_PROXY", "http://corp-proxy:3128") + + var buf bytes.Buffer + WarnIfProxied(&buf, false) + + if buf.Len() != 0 { + t.Errorf("expected no warning in non-interactive mode, got: %s", buf.String()) + } +} + // TestWarnIfProxied_SilentWhenDisabled verifies that LARK_CLI_NO_PROXY suppresses warnings. func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) @@ -88,7 +105,7 @@ func TestWarnIfProxied_SilentWhenDisabled(t *testing.T) { t.Setenv(EnvNoProxy, "1") var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) if buf.Len() != 0 { t.Errorf("expected no warning when proxy is disabled, got: %s", buf.String()) @@ -105,10 +122,10 @@ func TestWarnIfProxied_OnlyOnce(t *testing.T) { t.Setenv("HTTP_PROXY", "http://proxy:1234") var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) first := buf.String() - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) second := buf.String() if first == "" { @@ -137,7 +154,7 @@ func TestWarnIfProxied_ProxyPluginEnabled(t *testing.T) { t.Setenv(EnvNoProxy, "1") var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) out := buf.String() if !strings.Contains(out, "127.0.0.1:3128") { @@ -169,7 +186,7 @@ func TestWarnIfProxied_ProxyPluginCustomCAWarns(t *testing.T) { t.Cleanup(func() { proxyPluginStatus = old }) var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) out := buf.String() if !strings.Contains(out, "custom CA") { @@ -195,7 +212,7 @@ func TestWarnIfProxied_ProxyPluginEnabledRedactsCredentials(t *testing.T) { t.Cleanup(func() { proxyPluginStatus = old }) var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) out := buf.String() if strings.Contains(out, "s3cret") { @@ -243,7 +260,7 @@ func TestWarnIfProxied_RedactsCredentials(t *testing.T) { t.Setenv("HTTPS_PROXY", "http://admin:s3cret@proxy:8080") var buf bytes.Buffer - WarnIfProxied(&buf) + WarnIfProxied(&buf, true) out := buf.String() if bytes.Contains([]byte(out), []byte("s3cret")) {