Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,13 @@ func apiRun(opts *APIOptions) error {
return output.MarkRaw(client.WrapDoAPIError(err))
}
err = client.HandleResponse(resp, client.ResponseOptions{
OutputPath: opts.Output,
Format: format,
JqExpr: opts.JqExpr,
Out: out,
ErrOut: f.IOStreams.ErrOut,
FileIO: f.ResolveFileIO(opts.Ctx),
CommandPath: opts.Cmd.CommandPath(),
OutputPath: opts.Output,
Format: format,
JqExpr: opts.JqExpr,
Out: out,
ErrOut: f.IOStreams.ErrOut,
FileIO: f.ResolveFileIO(opts.Ctx),
})
// MarkRaw tells root error handler to skip enrichPermissionError,
// preserving the original API error detail (log_id, troubleshooter, etc.).
Expand Down
15 changes: 8 additions & 7 deletions cmd/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,14 @@ func serviceMethodRun(opts *ServiceMethodOptions) error {
return output.ErrNetwork("API call failed: %s", err)
}
return client.HandleResponse(resp, client.ResponseOptions{
OutputPath: opts.Output,
Format: format,
JqExpr: opts.JqExpr,
Out: out,
ErrOut: f.IOStreams.ErrOut,
FileIO: f.ResolveFileIO(opts.Ctx),
CheckError: checkErr,
CommandPath: opts.Cmd.CommandPath(),
OutputPath: opts.Output,
Format: format,
JqExpr: opts.JqExpr,
Out: out,
ErrOut: f.IOStreams.ErrOut,
FileIO: f.ResolveFileIO(opts.Ctx),
CheckError: checkErr,
})
}

Expand Down
30 changes: 30 additions & 0 deletions extension/contentsafety/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package contentsafety

import "sync"

var (
mu sync.Mutex
provider Provider
)

// Register installs the content-safety Provider. Later registrations
// override earlier ones (last-write-wins). The built-in regex provider
// registers itself from init() in internal/security/contentsafety when
// that package is blank-imported from main.go.
func Register(p Provider) {
mu.Lock()
defer mu.Unlock()
provider = p
}

// GetProvider returns the currently registered Provider, or nil if none
// is registered. A nil return value means "no scanning" and callers
// should treat it as a silent pass-through.
func GetProvider() Provider {
mu.Lock()
defer mu.Unlock()
return provider
}
57 changes: 57 additions & 0 deletions extension/contentsafety/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package contentsafety

import "context"

// Provider scans parsed response data for content-safety issues.
// Implementations must be safe for concurrent use.
type Provider interface {
// Name returns a stable provider identifier. Used in Alert payloads
// and diagnostic output.
Name() string

// Scan inspects req.Data and returns a non-nil Alert when any issue
// is detected, or nil when the data is clean.
//
// Returning a non-nil error signals that the scan itself failed
// (misconfiguration, transient I/O, internal panic). Callers are
// expected to treat scan errors as fail-open.
//
// Scan must respect ctx cancellation and return promptly once
// ctx.Err() becomes non-nil; callers may impose a deadline.
Scan(ctx context.Context, req ScanRequest) (*Alert, error)
}

// ScanRequest carries the data to be scanned plus minimal context.
type ScanRequest struct {
// CmdPath is the normalized command path (e.g. "im.messages_search").
// Providers may use it for per-command logic; most can ignore it.
CmdPath string

// Data is the parsed response payload as it flows through the CLI's
// output layer. It may be a map, slice, string, or a typed struct
// depending on the originating command. Providers must not mutate it.
// Providers that require a uniform shape should perform their own
// normalization internally.
Data any
}

// Alert describes content-safety issues discovered by a Provider.
// An Alert only exists when at least one issue was found; nil means clean.
type Alert struct {
// Provider identifies which provider produced this alert.
Provider string `json:"provider"`

// Matches is the list of issues detected. Guaranteed non-empty
// when the enclosing *Alert is non-nil.
Matches []RuleMatch `json:"matches"`
}

// RuleMatch describes a single rule hit.
type RuleMatch struct {
// Rule is the stable identifier of the matched rule
// (e.g. "instruction_override", "role_injection").
Rule string `json:"rule"`
}
43 changes: 43 additions & 0 deletions extension/contentsafety/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package contentsafety

import (
"context"
"testing"
)

type stubProvider struct{ name string }

func (s *stubProvider) Name() string { return s.name }
func (s *stubProvider) Scan(_ context.Context, _ ScanRequest) (*Alert, error) {
return nil, nil
}

func TestGetProvider_NilByDefault(t *testing.T) {
mu.Lock()
saved := provider
provider = nil
mu.Unlock()
t.Cleanup(func() { mu.Lock(); provider = saved; mu.Unlock() })

if got := GetProvider(); got != nil {
t.Fatalf("expected nil, got %v", got)
}
}

func TestRegister_LastWriteWins(t *testing.T) {
mu.Lock()
saved := provider
mu.Unlock()
t.Cleanup(func() { mu.Lock(); provider = saved; mu.Unlock() })

Register(&stubProvider{name: "a"})
Register(&stubProvider{name: "b"})

got := GetProvider()
if got == nil || got.Name() != "b" {
t.Fatalf("expected provider 'b', got %v", got)
}
}
26 changes: 15 additions & 11 deletions internal/client/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import (

// ResponseOptions configures how HandleResponse routes a raw API response.
type ResponseOptions struct {
OutputPath string // --output flag; "" = auto-detect
Format output.Format // output format for JSON responses
JqExpr string // if set, apply jq filter instead of Format
Out io.Writer // stdout
ErrOut io.Writer // stderr
FileIO fileio.FileIO // file transfer abstraction; required when saving files (--output or binary response)
CommandPath string // raw cobra CommandPath(); used by content-safety scanning
OutputPath string // --output flag; "" = auto-detect
Format output.Format // output format for JSON responses
JqExpr string // if set, apply jq filter instead of Format
Out io.Writer // stdout
ErrOut io.Writer // stderr
FileIO fileio.FileIO // file transfer abstraction; required when saving files (--output or binary response)
// CheckError is called on parsed JSON results. Nil defaults to CheckLarkResponse.
CheckError func(interface{}) error
}
Expand Down Expand Up @@ -63,11 +64,14 @@ func HandleResponse(resp *larkcore.ApiResp, opts ResponseOptions) error {
if opts.OutputPath != "" {
return saveAndPrint(opts.FileIO, resp, opts.OutputPath, opts.Out)
}
if opts.JqExpr != "" {
return output.JqFilter(opts.Out, result, opts.JqExpr)
}
output.FormatValue(opts.Out, result, opts.Format)
return nil
return output.EmitLarkResponse(output.LarkResponseEmitRequest{
CommandPath: opts.CommandPath,
Data: result,
Format: opts.Format.String(),
JqExpr: opts.JqExpr,
Out: opts.Out,
ErrOut: opts.ErrOut,
})
}

// Non-JSON (binary) responses.
Expand Down
1 change: 1 addition & 0 deletions internal/cmdutil/factory_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/keychain"
"github.com/larksuite/cli/internal/registry"
_ "github.com/larksuite/cli/internal/security/contentsafety" // register default content-safety provider
"github.com/larksuite/cli/internal/util"
_ "github.com/larksuite/cli/internal/vfs/localfileio" // register default FileIO provider
)
Expand Down
3 changes: 3 additions & 0 deletions internal/envvars/envvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ const (
CliTenantAccessToken = "LARKSUITE_CLI_TENANT_ACCESS_TOKEN"
CliDefaultAs = "LARKSUITE_CLI_DEFAULT_AS"
CliStrictMode = "LARKSUITE_CLI_STRICT_MODE"

CliContentSafetyMode = "LARKSUITE_CLI_CONTENT_SAFETY_MODE"
CliContentSafetyAllowlist = "LARKSUITE_CLI_CONTENT_SAFETY_ALLOWLIST"
)
Loading
Loading