mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
Compare commits
2 Commits
docs-skill
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2b4bedb7b | ||
|
|
35bc9517ce |
@@ -6,12 +6,10 @@ package cmdutil
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
@@ -40,8 +38,6 @@ const (
|
||||
BuildKindUnknown = "unknown"
|
||||
|
||||
officialModulePath = "github.com/larksuite/cli"
|
||||
|
||||
agentTraceMaxLen = 1024
|
||||
)
|
||||
|
||||
// UserAgentValue returns the User-Agent value: "lark-cli/{version}".
|
||||
@@ -49,25 +45,6 @@ func UserAgentValue() string {
|
||||
return SourceValue + "/" + build.Version
|
||||
}
|
||||
|
||||
// AgentTraceValue returns a header-safe value from the
|
||||
// LARKSUITE_CLI_AGENT_TRACE environment variable. It trims
|
||||
// surrounding whitespace, rejects values containing any Unicode
|
||||
// control character or exceeding agentTraceMaxLen, and returns ""
|
||||
// for any invalid or empty value. Callers can use the result
|
||||
// directly in HTTP headers without further sanitisation.
|
||||
func AgentTraceValue() string {
|
||||
v := strings.TrimSpace(os.Getenv(envvars.CliAgentTrace))
|
||||
if v == "" || len(v) > agentTraceMaxLen {
|
||||
return ""
|
||||
}
|
||||
for _, r := range v {
|
||||
if unicode.IsControl(r) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// BaseSecurityHeaders returns headers that every request must carry.
|
||||
func BaseSecurityHeaders() http.Header {
|
||||
h := make(http.Header)
|
||||
@@ -75,7 +52,7 @@ func BaseSecurityHeaders() http.Header {
|
||||
h.Set(HeaderVersion, build.Version)
|
||||
h.Set(HeaderBuild, DetectBuildKind())
|
||||
h.Set(HeaderUserAgent, UserAgentValue())
|
||||
if v := AgentTraceValue(); v != "" {
|
||||
if v := envvars.AgentTrace(); v != "" {
|
||||
h.Set(HeaderAgentTrace, v)
|
||||
}
|
||||
return h
|
||||
|
||||
@@ -6,7 +6,6 @@ package cmdutil
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/extension/credential"
|
||||
@@ -264,88 +263,9 @@ func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentTraceValue / HeaderAgentTrace
|
||||
// HeaderAgentTrace injection (via BaseSecurityHeaders)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestAgentTraceValue_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "trace-abc-123")
|
||||
if got := AgentTraceValue(); got != "trace-abc-123" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %q", got, "trace-abc-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, " trace-trim ")
|
||||
if got := AgentTraceValue(); got != "trace-trim" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %q (whitespace trimmed)", got, "trace-trim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_OnlyWhitespace_ReturnsEmpty(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, " ")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for whitespace-only value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsCRLF(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\r\nX-Evil: attack")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsLF(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\nX-Evil: attack")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsTab(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\tinjected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for tab value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\x01injected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsDEL(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "val\x7finjected")
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() = %q, want empty for DEL value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentTraceMaxLen+1)
|
||||
t.Setenv(envvars.CliAgentTrace, longVal)
|
||||
if got := AgentTraceValue(); got != "" {
|
||||
t.Fatalf("AgentTraceValue() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTraceValue_AcceptsMaxLengthValue(t *testing.T) {
|
||||
val := strings.Repeat("a", agentTraceMaxLen)
|
||||
t.Setenv(envvars.CliAgentTrace, val)
|
||||
if got := AgentTraceValue(); got != val {
|
||||
t.Fatalf("AgentTraceValue() = %q, want %d-byte value accepted", got, agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseSecurityHeaders_NoAgentTraceHeaderWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(envvars.CliAgentTrace, "")
|
||||
h := BaseSecurityHeaders()
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
// Content safety scanning mode
|
||||
CliContentSafetyMode = "LARKSUITE_CLI_CONTENT_SAFETY_MODE"
|
||||
|
||||
CliAgentName = "LARKSUITE_CLI_AGENT_NAME"
|
||||
CliAgentTrace = "LARKSUITE_CLI_AGENT_TRACE"
|
||||
|
||||
CliProxyEnable = "LARKSUITE_CLI_PROXY_ENABLE"
|
||||
|
||||
36
internal/envvars/read.go
Normal file
36
internal/envvars/read.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envvars
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
agentNameMaxLen = 128
|
||||
agentTraceMaxLen = 1024
|
||||
)
|
||||
|
||||
func AgentName() string {
|
||||
return sanitizeSingleLine(os.Getenv(CliAgentName), agentNameMaxLen)
|
||||
}
|
||||
|
||||
func AgentTrace() string {
|
||||
return sanitizeSingleLine(os.Getenv(CliAgentTrace), agentTraceMaxLen)
|
||||
}
|
||||
|
||||
func sanitizeSingleLine(raw string, maxLen int) string {
|
||||
v := strings.TrimSpace(raw)
|
||||
if v == "" || len(v) > maxLen {
|
||||
return ""
|
||||
}
|
||||
for _, r := range v {
|
||||
if unicode.IsControl(r) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
131
internal/envvars/read_test.go
Normal file
131
internal/envvars/read_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package envvars
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgentName_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "claude-code")
|
||||
if got := AgentName(); got != "claude-code" {
|
||||
t.Fatalf("AgentName() = %q, want %q", got, "claude-code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(CliAgentName, " cursor ")
|
||||
if got := AgentName(); got != "cursor" {
|
||||
t.Fatalf("AgentName() = %q, want %q (whitespace trimmed)", got, "cursor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsCRLFInjection(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "agent\r\nX-Evil: attack")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(CliAgentName, "agent\x01injected")
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentName_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentNameMaxLen+1)
|
||||
t.Setenv(CliAgentName, longVal)
|
||||
if got := AgentName(); got != "" {
|
||||
t.Fatalf("AgentName() returned non-empty for %d-byte value (max %d)", len(longVal), agentNameMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_EmptyWhenEnvUnset(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty when env unset", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_ReturnsCleanValue(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "trace-abc-123")
|
||||
if got := AgentTrace(); got != "trace-abc-123" {
|
||||
t.Fatalf("AgentTrace() = %q, want %q", got, "trace-abc-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_TrimsWhitespace(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, " trace-trim ")
|
||||
if got := AgentTrace(); got != "trace-trim" {
|
||||
t.Fatalf("AgentTrace() = %q, want %q (whitespace trimmed)", got, "trace-trim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_OnlyWhitespace_ReturnsEmpty(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, " ")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for whitespace-only value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsCRLF(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\r\nX-Evil: attack")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for CR/LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsLF(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\nX-Evil: attack")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for LF value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsTab(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\tinjected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for tab value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsControlChar(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\x01injected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for control char value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsDEL(t *testing.T) {
|
||||
t.Setenv(CliAgentTrace, "val\x7finjected")
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() = %q, want empty for DEL value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_RejectsOverlongValue(t *testing.T) {
|
||||
longVal := strings.Repeat("a", agentTraceMaxLen+1)
|
||||
t.Setenv(CliAgentTrace, longVal)
|
||||
if got := AgentTrace(); got != "" {
|
||||
t.Fatalf("AgentTrace() returned non-empty for %d-byte value (max %d)", len(longVal), agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentTrace_AcceptsMaxLengthValue(t *testing.T) {
|
||||
val := strings.Repeat("a", agentTraceMaxLen)
|
||||
t.Setenv(CliAgentTrace, val)
|
||||
if got := AgentTrace(); got != val {
|
||||
t.Fatalf("AgentTrace() = %q, want %d-byte value accepted", got, agentTraceMaxLen)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user