# Embed Lark CLI in an agent

## When to use

Lark CLI comes with a built-in **extension mechanism** that lets you replace or enhance the CLI’s core behavior **without modifying the CLI source code**.

If you’re an in-house enterprise developer or an ISV and want to **embed Lark** **CLI** **capabilities** directly into your own Agent or application, and you need to:
- Pull AppID / Token (and more) from your own credential system (database, Vault, config center)
- Apply unified monitoring and logging to all HTTP requests, or rewrite request targets, etc.

All you need to do is: write a Go package that implements the required interfaces, import it in a wrapper `main`, and rebuild—then you get an **enhanced** **CLI** with **completely different behavior**.

Two extension points are supported today:

- **Credential — credential source**: The CLI needs an AppID, plus a real AppSecret or a usable UAT/TAT to call Lark APIs. By default, it reads from environment variables or the local keychain. It only strictly depends on AppSecret when exchanging `tenant_access_token`. With the Credential extension, you can fetch credentials from any source (database, Vault, config center, etc.). For clarity, this article demonstrates reading credentials locally.

- **Transport — HTTP request interception**: Every HTTP request from the CLI goes through the Transport layer. With the Transport extension, you can inject headers, rewrite request targets (Host / URL), and record logs before sending a request; and you can measure latency and report metrics after the request completes.

## Extension 1: Credential - credential source

### What it is

The Credential extension lets you **customize where credentials come from**. By default, the CLI reads credentials from environment variables (`LARKSUITE_CLI_APP_ID`, `LARKSUITE_CLI_APP_SECRET`, `LARKSUITE_CLI_USER_ACCESS_TOKEN`, `LARKSUITE_CLI_TENANT_ACCESS_TOKEN`) or the local keychain. By registering a custom Provider, you can pull credentials from any source (database, Vault, config center, etc.). For clarity, this article demonstrates reading credentials locally.

### How to use it

#### Step 1: Create a project

```shell 
mkdir my-cli && cd my-cli
go mod init my-cli 
``` 

#### Step 2: Implement a provider
Create `mycred/mycred.go` and implement the `credential.Provider` interface:

```go
package mycred
import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "github.com/larksuite/cli/extension/credential"
)
const (
    appID     = "cli_aXXXXXXXXXXXXXXXX" // Replace with real AppID
    appSecret = "XXXXXXXXXXXXXXXXXXXXXXXX" // Replace with real AppSecret
)
type Provider struct{}
func (p *Provider) Name() string { return "mycred" }
// ResolveAccount returns app credentials.
// Returns &Account{} → Hit; the CLI uses these credentials.
// Returns nil, nil   → Skip; pass to the next Provider.
// Returns nil, err   → Error; abort.
func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, error) {
    // Replace with your logic: query DB, call Vault, read config center...
    return &credential.Account{
        AppID:     appID,
        AppSecret: appSecret,
        Brand:     credential.BrandLark,
        DefaultAs: credential.IdentityUser,
    }, nil
}
// ResolveToken returns the access token.
// Important: Once your Provider takes over the Account, you own the Token.
// Returning nil, nil does not fall back to the default token exchange logic; it errors out directly.
func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) {
    switch req.Type {
    case credential.TokenTypeTAT:
        // Bot identity: use AppID + AppSecret to exchange a tenant_access_token from Lark
        token, err := exchangeTenantAccessToken(appID, appSecret)
        if err != nil {
            return nil, err
        }
        return &credential.Token{Value: token, Source: "mycred:tat"}, nil
    case credential.TokenTypeUAT:
        // User identity: fetch a user_access_token from any source
        // For example: query a DB, read from Redis cache, get from an OAuth callback,
        // read from a config file, or even hardcode one for testing...
        uat := getUserAccessToken()
        return &credential.Token{Value: uat, Source: "mycred:uat"}, nil
    }
    return nil, nil
}
// getUserAccessToken fetches a UAT from your source.
// You can integrate with any storage here: DB, Redis, file, env vars, etc.
func getUserAccessToken() string {
    // Example: fetch from env vars
    // return os.Getenv("MY_USER_ACCESS_TOKEN")
    // Example: query from DB
    // return db.Query("SELECT uat FROM tokens WHERE user_id = ?", userID)
    // Example: hardcode one for local testing (obtained after 'lark-cli auth login')
    return "u-XXXXXXXX"
}
// exchangeTenantAccessToken uses AppID + AppSecret to exchange a tenant_access_token from Lark.
func exchangeTenantAccessToken(appID, appSecret string) (string, error) {
    body, _ := json.Marshal(map[string]string{
        "app_id":     appID,
        "app_secret": appSecret,
    })
    resp, err := http.Post(
        // Using the Lark brand here as a demonstration. For the Lark brand, switch the host to: open.larksuite.com
        "https://open.larksuite.com/open-apis/auth/v3/tenant_access_token/internal",
        "application/json", bytes.NewReader(body),
    )
    if err != nil {
        return "", fmt.Errorf("exchange tat: %w", err)
    }
    defer resp.Body.Close()
    var result struct {
        Code              int    `json:"code"`
        TenantAccessToken string `json:"tenant_access_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", err
    }
    if result.Code != 0 {
        return "", fmt.Errorf("exchange tat failed, code=%d", result.Code)
    }
    return result.TenantAccessToken, nil
}
func init() {
    credential.Register(&Provider{})
}
```

#### Step 3: Write a wrapper main

Create main.go:

```go
package main
import (
    "os"
    "github.com/larksuite/cli/cmd"
    _ "my-cli/mycred"  // blank import triggers init() → registers your Provider
)
func main() {
    os.Exit(cmd.Execute())
}
```

#### Step 4: Build and verify

```shell 
go mod tidy
go build -o my-lark-cli . 

# Use --dry-run to verify the credential source (no real request is sent)
./my-lark-cli api GET /open-apis/calendar/v4/calendars --dry-run

``` 

Output:

```json 
{
  "appId": "cli_a1234567890",
  "as": "user"
} 
``` 

If appId shows the value you set, your custom Provider is working.

### How it works

Credential stores multiple Providers in a chained array. The CLI queries them in registration order. The first Provider that returns a non-nil result wins:

```json 
import order:  mycred → env
registration:  providers = [mycred, env]
when CLI needs credentials:
  ┌─ mycred.ResolveAccount()
  │   found?     → return Account → hit! skip the rest
  │   not found? → return nil     → continue
  │
  ├─ env.ResolveAccount()
  │   has env?   → return Account → hit
  │   no env?    → return nil     → continue
  │
  └─ defaultAcct (keychain fallback) 
``` 
**Lazy execution**: Providers only register at startup—they are not called immediately. The chained lookup only runs when a user command actually needs credentials. The result is cached with sync.Once, so the process resolves credentials only once.

#### Core source files
| File                                 | Purpose                                      |
| ---------------------------------- | --------------------------------------- |
| `extension/credential/types.go`    | Provider interface, Account / Token type definitions        |
| `extension/credential/registry.go` | `Register()` appends to the global array; `Providers()` returns a snapshot |
| `extension/credential/env/env.go`  | Built-in implementation: reads credentials from environment variables|

## Extension 2: Transport — HTTP request interception
### What it is
The Transport extension lets you **intercept every HTTP request sent by the CLI**. You can modify headers, rewrite request targets (Host / URL), and print logs before a request is sent, and record latency and report metrics after the request completes—without modifying the CLI's business logic at all.

### How to use
#### Step 1: Implement an interceptor

Create `tracing/tracing.go` and implement the `transport.Provider` and `transport.Interceptor` interfaces:

```go
package tracing
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "time"
    "github.com/larksuite/cli/extension/transport"
)
type Provider struct{}
func (p *Provider) Name() string { return "tracing" }
func (p *Provider) ResolveInterceptor(ctx context.Context) transport.Interceptor {
    return &tracingInterceptor{}
}
type tracingInterceptor struct{}
// PreRoundTrip is called before each request is sent.
// The returned function is called after the request completes.
func (i *tracingInterceptor) PreRoundTrip(req *http.Request) func(*http.Response, error) {
    // ===== Before the request =====
    start := time.Now()
    fmt.Fprintf(os.Stderr, "🚀 Request started: %s %s\n", req.Method, req.URL)
    // ===== After the request (returned closure) =====
    return func(resp *http.Response, err error) {
        elapsed := time.Since(start)
        if err != nil {
            fmt.Fprintf(os.Stderr, "❌ Request failed: %s %s err=%v (latency %v)\n",
                req.Method, req.URL, err, elapsed)
        } else {
            fmt.Fprintf(os.Stderr, "✅ Request completed: %s %s status=%d (latency %v)\n",
                req.Method, req.URL, resp.StatusCode, elapsed)
        }
    }
}
func init() {
    transport.Register(&Provider{})
}
```

#### Step 2: Import It in main.go

```go
package main
import (
    "os"
    "github.com/larksuite/cli/cmd"
    _ "my-cli/tracing"  // blank import registers the Transport interceptor
    _ "github.com/larksuite/cli/extension/credential/env"
)
func main() {
    os.Exit(cmd.Execute())
}
```

#### Step 3: Build and verify

```shell
go build -o my-lark-cli .
```
- **Verify UAT (user identity)**:

```shell 
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as user 
``` 

Output:

```json 
🚀 Request started: GET https://open.larksuite.com/open-apis/authen/v1/user_info
✅ Request completed: GET https://open.larksuite.com/open-apis/authen/v1/user_info status=200 (latency 644ms)
🚀 Request started: GET https://open.larksuite.com/open-apis/calendar/v4/calendars
✅ Request completed: GET https://open.larksuite.com/open-apis/calendar/v4/calendars status=200 (latency 1070ms)
{ "code": 0, "data": { "calendar_list": [...] } } 
``` 

- **Verify TAT (bot identity):**

```shell 
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as bot 
``` 

Output:

```json 
🚀 Request started: GET https://open.larksuite.com/open-apis/calendar/v4/calendars
✅ Request completed: GET https://open.larksuite.com/open-apis/calendar/v4/calendars status=200 (latency 736ms)
{ "code": 0, "data": { ... } }
How It Works
Transport uses a single-Provider model. Upon registration, it is wrapped as an HTTP middleware and inserted into the CLI's request chain:
CLI sends an HTTP request
      │
      ▼
Your PreRoundTrip(req)         ← Before: add headers / rewrite host / log / start timer
      │  returns a postFn
      ▼
Built-in CLI transport chain   ← Retry → SecurityHeader → actual network request
      │
      ▼
postFn(resp, err)              ← After: record latency / report metrics 
``` 

When building the HTTP Client, the CLI calls wrapWithExtension() to wrap your Interceptor as middleware. After that, every request sent by the CLI (including token exchanges and business API calls) goes through this middleware.
**Security guardrails**: The CLI features a built-in security layer that consistently rewrites baseline security headers (such as X-Cli-* and User-Agent). 

Authorization is not covered by this unified overwrite: it is set during the request construction phase; if a cross-host redirect occurs, it is stripped by the redirect policy. Therefore, you can add custom headers, but ==do not rely on== the Transport extension to override credential headers.

#### Core source files

| File                                | Purpose                                          |
| --------------------------------- | ------------------------------------------- |
| `extension/transport/types.go`    | Provider / Interceptor interface definitions                 |
| `extension/transport/registry.go` | `Register()` registers a single Provider (last-write-wins) |

### Full example
A wrapper main combining both extensions:

```go 
package main
import (
    "os"
    "github.com/larksuite/cli/cmd"
    // Custom extensions (placed first = higher credential priority)
    _ "my-cli/mycred"    // Custom credential source
    _ "my-cli/tracing"   // HTTP request interception
    // Fallback to stock CLI
    _ "github.com/larksuite/cli/extension/credential/env"
)
func main() {
    os.Exit(cmd.Execute())
} 
``` 
Compared to the stock CLI main.go, this only adds 2 import lines. The business logic cmd.Execute() remains entirely unchanged, yet compiling it yields a fully functional, enhanced CLI.

### Project structure

```shell 
my-cli/
├── go.mod             # require github.com/larksuite/cli v1.0.6
├── main.go            # Wrapper main
├── mycred/
│   └── mycred.go      # Credential: custom credential source
└── tracing/
    └── tracing.go     # Transport: HTTP request interception
``` 

### Build and run

```shell 
mkdir my-cli && cd my-cli
go mod init my-cli 
# Write main.go, mycred/mycred.go, tracing/tracing.go (see code above)
go get github.com/larksuite/cli@v1.0.6
go mod tidy
go build -o my-lark-cli .
# Verify credential extension (--dry-run sends no actual request, only checks which credentials are used)
./my-lark-cli api GET /open-apis/calendar/v4/calendars --dry-run
``` 

Output:

```json 
{
  "appId": "cli_aXXXXXXXXXXXXXXXX",
  "as": "user"
} 
``` 

```shell 
# Verify request interception (sends an actual request to view log outputs)
./my-lark-cli api GET /open-apis/calendar/v4/calendars --as user 
``` 

Output:

```json 
🚀 Request started: GET https://open.larksuite.com/open-apis/authen/v1/user_info
✅ Request completed: GET .../user_info status=200 (latency 644ms)
🚀 Request started: GET https://open.larksuite.com/open-apis/calendar/v4/calendars
✅ Request completed: GET .../calendars status=200 (latency 1070ms)
{ "code": 0, "data": { "calendar_list": [...] } } 
``` 

Both extensions take effect simultaneously: Credential provides the credentials and Token, while Transport intercepts and logs every request.
