Skip to content

Commit 27a2f27

Browse files
authored
fix(config): make agent-binding hints workspace-aware and surface user-identity risks (#728)
AI agents running inside OpenClaw / Hermes were routinely creating a parallel app via `config init --new` instead of binding to the agent's existing app, because every "not configured" hint and several deny errors hard-coded `config init` regardless of workspace. Once bound, the same agents could silently grant themselves user identity (impersonation) without the user ever seeing a risk message in chat. Changes: - Introduce `core.NotConfiguredError` / `NoActiveProfileError` / `reconfigureHint` helpers that branch on `CurrentWorkspace()`. In agent workspaces they point at `lark-cli config bind --help` (a help page, not a ready-to-run command) so AI must read the binding workflow and confirm identity preset with the user before acting. In local terminals they preserve the previous `config init --new` guidance. - Migrate every `config init` hint that should be workspace-aware: RequireConfigForProfile, default credential provider, credential provider fallback, secret-resolve mismatch, config show, strict-mode entry-point errors, default-as, profile use/rename/remove, auth list, doctor's config_file check (which now also wraps the OS-level "no such file" noise into the user-shaped "not configured" message). - Refuse `config init` when run inside an OpenClaw / Hermes workspace by default; add `--force-init` for the rare case the user genuinely wants a parallel app. Without this guard, hint fixes were undone the moment AI ignored them. - Rewrite the strict-mode deny errors in cmd/auth/login.go, cmd/prune.go, and internal/cmdutil/factory.go. The previous "AI agents are strictly prohibited from modifying this setting" terminated AI reasoning while providing no real gate. New errors point at `config strict-mode --help` with the legitimate confirmation flow and explicitly note that switching does NOT require re-bind. Integration test envelopes updated. - Tighten `config bind --help` and `config strict-mode --help` to encode the user-confirmation discipline directly: identity preset semantics (bot-only vs user-default), "DO NOT switch without explicit user confirmation", and a cross-reference clarifying that `config bind` is for changing the underlying app while `config strict-mode` is the policy-only switch (resolves an ambiguity an audit run found). - Surface user-identity (impersonation) risk at every config write that newly grants it, by reusing the canonical IdentityEscalationMessage string from bind_messages.go: - `noticeUserDefaultRisk` fires on flag-mode bind landing on user-default, including the first-time case `warnIdentityEscalation` misses (it requires a previous bot lock). - `setStrictMode` warns when transitioning bot → user or bot → off (newly permits user identity); stays quiet on narrowing changes and on off → user (off already permitted user). - Add tests: notconfigured_test.go (workspace branches), init_guard_test.go (refuse + --force-init bypass), bind_warning_test.go (user-default warning fires; bot-only does not), strict_mode_warning_test.go (5 transitions covering both warn and no-warn paths). Two follow-ups intentionally deferred: the keychain master-key hint at internal/keychain/keychain.go:42 still suggests `config init` because the keychain package can't import core (would be circular); fixing requires either parameterizing the hint via callback or extracting workspace into its own package. The lark-shared skill doc still tells AI to run `config init` for first-time setup; updating the skill is in scope for a follow-up PR. Change-Id: I02273e044d9e061d211ceaa4f3ed5a3fb28325b3
1 parent 15ae1fa commit 27a2f27

29 files changed

Lines changed: 899 additions & 94 deletions

cmd/auth/list.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package auth
55

66
import (
7+
"errors"
78
"fmt"
89

910
"github.com/spf13/cobra"
@@ -42,7 +43,18 @@ func authListRun(opts *ListOptions) error {
4243

4344
multi, _ := core.LoadMultiAppConfig()
4445
if multi == nil || len(multi.Apps) == 0 {
45-
fmt.Fprintln(f.IOStreams.ErrOut, "Not configured yet. Run `lark-cli config init` to initialize.")
46+
// auth list is a read-only probe; the "configured but no users"
47+
// branch below already returns exit 0 with a stderr hint, so we
48+
// keep the same contract here. We still want the hint to be
49+
// workspace-aware, so we pull the message+hint out of
50+
// NotConfiguredError() instead of hard-coding it.
51+
var cfgErr *core.ConfigError
52+
if errors.As(core.NotConfiguredError(), &cfgErr) {
53+
fmt.Fprintln(f.IOStreams.ErrOut, cfgErr.Message)
54+
if cfgErr.Hint != "" {
55+
fmt.Fprintln(f.IOStreams.ErrOut, " hint: "+cfgErr.Hint)
56+
}
57+
}
4658
return nil
4759
}
4860

cmd/auth/list_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"github.com/larksuite/cli/internal/cmdutil"
11+
"github.com/larksuite/cli/internal/core"
12+
)
13+
14+
// TestAuthListRun_NotConfigured_ReturnsExitZero pins the contract that
15+
// `lark-cli auth list` is a read-only probe and must not fail-hard when no
16+
// config exists yet — scripts and AI agents use it as an idempotent "do I
17+
// have any users?" check, so the exit code carries semantic weight. Pair
18+
// that with the existing "configured but no logged-in users" branch (also
19+
// exit 0) and both empty states are consistent.
20+
func TestAuthListRun_NotConfigured_ReturnsExitZero(t *testing.T) {
21+
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
22+
23+
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
24+
if err := authListRun(&ListOptions{Factory: f}); err != nil {
25+
t.Fatalf("auth list should succeed when not configured (exit 0); got: %v", err)
26+
}
27+
// Local workspace → hint must mention init, not bind.
28+
out := stderr.String()
29+
if !strings.Contains(out, "config init") {
30+
t.Errorf("local hint missing config init: %s", out)
31+
}
32+
if strings.Contains(out, "config bind") {
33+
t.Errorf("local hint must not mention config bind: %s", out)
34+
}
35+
}
36+
37+
// TestAuthListRun_NotConfigured_AgentWorkspace_RoutesToBindHelp covers the
38+
// reason this hint exists workspace-aware in the first place: an AI agent
39+
// in OpenClaw / Hermes that probes auth list before binding gets routed to
40+
// `config bind --help` instead of the local-only `config init`.
41+
func TestAuthListRun_NotConfigured_AgentWorkspace_RoutesToBindHelp(t *testing.T) {
42+
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
43+
44+
prev := core.CurrentWorkspace()
45+
t.Cleanup(func() { core.SetCurrentWorkspace(prev) })
46+
core.SetCurrentWorkspace(core.WorkspaceOpenClaw)
47+
48+
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
49+
if err := authListRun(&ListOptions{Factory: f}); err != nil {
50+
t.Fatalf("auth list should still succeed under agent workspace; got: %v", err)
51+
}
52+
out := stderr.String()
53+
if !strings.Contains(out, "config bind --help") {
54+
t.Errorf("agent hint must point at config bind --help: %s", out)
55+
}
56+
if strings.Contains(out, "config init") {
57+
t.Errorf("agent hint must not mention config init: %s", out)
58+
}
59+
}

cmd/auth/login.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ For AI agents: this command blocks until the user completes authorization in the
4949
browser. Run it in the background and retrieve the verification URL from its output.`,
5050
RunE: func(cmd *cobra.Command, args []string) error {
5151
if mode := f.ResolveStrictMode(cmd.Context()); mode == core.StrictModeBot {
52-
return output.Errorf(output.ExitValidation, "strict_mode",
53-
"strict mode is %q, user login is not allowed. "+
54-
"This setting is managed by the administrator and must not be modified by AI agents.",
55-
mode)
52+
return output.ErrWithHint(output.ExitValidation, "strict_mode",
53+
fmt.Sprintf("strict mode is %q, user login is disabled in this profile", mode),
54+
"if the user explicitly wants to switch to user identity, see `lark-cli config strict-mode --help` (confirm with the user before switching; switching does NOT require re-bind)")
5655
}
5756
opts.Ctx = cmd.Context()
5857
if runF != nil {
@@ -243,14 +242,19 @@ func authLoginRun(opts *LoginOptions) error {
243242
return nil
244243
}
245244

246-
// Step 2: Show user code and verification URL
245+
// Step 2: Show user code and verification URL.
246+
// Both branches surface AgentTimeoutHint, but on different channels:
247+
// JSON mode embeds it as a structured field (so an agent that captures
248+
// stdout into a JSON parser sees it without stream-mixing surprises),
249+
// text mode prints to stderr (alongside the URL prompt).
247250
if opts.JSON {
248251
data := map[string]interface{}{
249252
"event": "device_authorization",
250253
"verification_uri": authResp.VerificationUri,
251254
"verification_uri_complete": authResp.VerificationUriComplete,
252255
"user_code": authResp.UserCode,
253256
"expires_in": authResp.ExpiresIn,
257+
"agent_hint": msg.AgentTimeoutHint,
254258
}
255259
encoder := json.NewEncoder(f.IOStreams.Out)
256260
encoder.SetEscapeHTML(false)
@@ -260,6 +264,7 @@ func authLoginRun(opts *LoginOptions) error {
260264
} else {
261265
fmt.Fprintf(f.IOStreams.ErrOut, msg.OpenURL)
262266
fmt.Fprintf(f.IOStreams.ErrOut, " %s\n\n", authResp.VerificationUriComplete)
267+
fmt.Fprintln(f.IOStreams.ErrOut, msg.AgentTimeoutHint)
263268
}
264269

265270
// Step 3: Poll for token
@@ -346,6 +351,12 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
346351
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to remove cached requested scopes: %v\n", err)
347352
}
348353
}
354+
// Skip the stderr hint in JSON mode — the --no-wait call that issued the
355+
// device_code already returned the hint as a JSON field, and writing
356+
// text to stderr would pollute consumers that combine streams via 2>&1.
357+
if !opts.JSON {
358+
fmt.Fprintln(f.IOStreams.ErrOut, msg.AgentTimeoutHint)
359+
}
349360
log(msg.WaitingAuth)
350361
result := pollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
351362
opts.DeviceCode, 5, 600, f.IOStreams.ErrOut)

cmd/auth/login_messages.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type loginMsg struct {
2222
// Non-interactive prompts (login.go)
2323
OpenURL string
2424
WaitingAuth string
25+
AgentTimeoutHint string
2526
AuthSuccess string
2627
LoginSuccess string
2728
AuthorizedUser string
@@ -58,6 +59,7 @@ var loginMsgZh = &loginMsg{
5859

5960
OpenURL: "在浏览器中打开以下链接进行认证:\n\n",
6061
WaitingAuth: "等待用户授权...",
62+
AgentTimeoutHint: "[AI agent] 此命令最长阻塞约 10 分钟,等待用户在浏览器内完成授权。请确保 runner 的 timeout ≥ 600s;如不支持长 timeout,请改用 `lark-cli auth login --no-wait --json` 拿到 device_code 后再用 `lark-cli auth login --device-code <code>` 续上轮询,**不要短 timeout 反复重试**——每次重启会作废上一轮的 device code,导致用户授权的链接失效。",
6163
AuthSuccess: "已收到授权确认,正在获取用户信息并校验授权结果...",
6264
LoginSuccess: "授权成功! 用户: %s (%s)",
6365
AuthorizedUser: "当前授权账号: %s (%s)",
@@ -93,6 +95,7 @@ var loginMsgEn = &loginMsg{
9395

9496
OpenURL: "Open this URL in your browser to authenticate:\n\n",
9597
WaitingAuth: "Waiting for user authorization...",
98+
AgentTimeoutHint: "[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is ≥ 600s. If long timeouts are not supported, use `lark-cli auth login --no-wait --json` to get a device_code, then `lark-cli auth login --device-code <code>` to resume polling. **Do NOT retry with a short timeout** — each restart invalidates the previous device code, so any URL the user already authorized becomes useless.",
9699
AuthSuccess: "Authorization confirmed, fetching user info and validating granted scopes...",
97100
LoginSuccess: "Authorization successful! User: %s (%s)",
98101
AuthorizedUser: "Authorized account: %s (%s)",

cmd/auth/login_messages_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package auth
66
import (
77
"fmt"
88
"reflect"
9+
"strings"
910
"testing"
1011
)
1112

@@ -94,3 +95,21 @@ func TestLoginMsg_FormatStrings(t *testing.T) {
9495
}
9596
}
9697
}
98+
99+
// TestAgentTimeoutHint_CarriesKeyInfo guards the contract that the synchronous
100+
// auth-login output tells AI agents two things: (a) this command blocks for
101+
// minutes — set a long runner timeout, and (b) the alternative is the
102+
// --no-wait + --device-code split-flow. Without (a) AI sets a 10s timeout and
103+
// kills the process before the user can authorize; without (b) the AI has no
104+
// recovery path and just retries with the same short timeout, invalidating
105+
// each new device code in turn.
106+
func TestAgentTimeoutHint_CarriesKeyInfo(t *testing.T) {
107+
for _, lang := range []string{"zh", "en"} {
108+
hint := getLoginMsg(lang).AgentTimeoutHint
109+
for _, want := range []string{"--no-wait", "--device-code"} {
110+
if !strings.Contains(hint, want) {
111+
t.Errorf("%s AgentTimeoutHint missing %q: %s", lang, want, hint)
112+
}
113+
}
114+
}
115+
}

cmd/config/bind.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,32 @@ func NewCmdConfigBind(f *cmdutil.Factory, runF func(*BindOptions) error) *cobra.
6262
Short: "Bind Agent config to a workspace (source / app-id / force)",
6363
Long: `Bind an AI Agent's (OpenClaw / Hermes) Feishu credentials to a lark-cli workspace.
6464
65-
For AI agents: pass --source and --app-id to bind non-interactively.
66-
Credentials are synced once; subsequent calls in the Agent's process
67-
context automatically use the bound workspace.`,
68-
Example: ` lark-cli config bind --source openclaw --app-id <id>
69-
lark-cli config bind --source hermes`,
65+
--source is auto-detected from env (OPENCLAW_HOME / HERMES_HOME); pass it only to override.
66+
67+
For AI agents — DO NOT bind without user confirmation. Binding may
68+
overwrite an existing one and locks in an identity policy. Ask the user:
69+
70+
--identity bot-only bot only (safer default; no impersonation;
71+
cannot access user resources like personal
72+
calendar / mail / drive)
73+
--identity user-default user identity allowed (impersonates the user;
74+
needed for personal-resource access)
75+
76+
Default to bot-only if the user is unsure. Only run the command after
77+
the user confirms both intent and identity preset.
78+
79+
If lark-cli is already bound and the user only wants to change identity
80+
policy on the SAME app, use 'config strict-mode' — that's the policy
81+
switch and does not require re-bind. Use 'config bind' only when the
82+
underlying app itself changes.
83+
84+
Interactive terminal use: run with no flags to enter the TUI form.`,
85+
Example: ` # AI flow: confirm intent + identity with user FIRST, then run:
86+
lark-cli config bind --source openclaw --app-id <id> --identity bot-only
87+
lark-cli config bind --source hermes --identity user-default
88+
89+
# Interactive (terminal user) — TUI prompts for everything:
90+
lark-cli config bind`,
7091
RunE: func(cmd *cobra.Command, args []string) error {
7192
opts.langExplicit = cmd.Flags().Changed("lang")
7293
if runF != nil {
@@ -125,6 +146,7 @@ func configBindRun(opts *BindOptions) error {
125146
return err
126147
}
127148
applyPreferences(appConfig, opts)
149+
noticeUserDefaultRisk(opts)
128150

129151
return commitBinding(opts, appConfig, existing.ConfigBytes, source, targetConfigPath)
130152
}
@@ -308,6 +330,23 @@ func warnIdentityEscalation(opts *BindOptions, previousConfigBytes []byte) error
308330
msg.IdentityEscalationMessage, msg.IdentityEscalationHint)
309331
}
310332

333+
// noticeUserDefaultRisk surfaces the user-identity impersonation risk on every
334+
// flag-mode bind that lands on user-default. The bot-only → user-default
335+
// escalation is already covered by warnIdentityEscalation (errors out before
336+
// applyPreferences runs), and the TUI flow shows IdentityUserDefaultDesc
337+
// during identity selection — so this fires specifically for the case those
338+
// two miss: a fresh flag-mode bind that goes directly to user-default with
339+
// no previous bot lock to escalate from. Without this, AI agents finish such
340+
// a bind with only a "配置成功" message and never relay to the user that the
341+
// AI can now act under their identity.
342+
func noticeUserDefaultRisk(opts *BindOptions) {
343+
if opts.IsTUI || opts.Identity != "user-default" {
344+
return
345+
}
346+
msg := getBindMsg(opts.Lang)
347+
fmt.Fprintln(opts.Factory.IOStreams.ErrOut, "⚠️ "+msg.IdentityEscalationMessage)
348+
}
349+
311350
// applyPreferences expands the chosen identity preset into the underlying
312351
// StrictMode + DefaultAs on the AppConfig. Always writes both fields so the
313352
// profile's intent survives later changes to global strict-mode settings.

cmd/config/bind_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,16 +377,28 @@ func TestConfigShowRun_AgentWorkspaceNotBound(t *testing.T) {
377377
if err == nil {
378378
t.Fatal("expected error for unbound workspace")
379379
}
380-
var exitErr *output.ExitError
381-
if !errors.As(err, &exitErr) {
382-
t.Fatalf("error type = %T, want *output.ExitError", err)
380+
// Should be a structured ConfigError suggesting config bind, not config init.
381+
var cfgErr *core.ConfigError
382+
if !errors.As(err, &cfgErr) {
383+
t.Fatalf("error type = %T, want *core.ConfigError", err)
384+
}
385+
if cfgErr.Code != output.ExitValidation {
386+
t.Errorf("exit code = %d, want %d", cfgErr.Code, output.ExitValidation)
387+
}
388+
if cfgErr.Type != "openclaw" {
389+
t.Errorf("type = %q, want %q", cfgErr.Type, "openclaw")
390+
}
391+
if !strings.Contains(cfgErr.Message, "openclaw context detected") {
392+
t.Errorf("message missing 'openclaw context detected': %q", cfgErr.Message)
393+
}
394+
// Hint must point at config bind --help (NOT a ready-to-run bind command):
395+
// AI must read the help and confirm identity preset with the user first.
396+
if !strings.Contains(cfgErr.Hint, "config bind --help") {
397+
t.Errorf("hint must point at `config bind --help`; got %q", cfgErr.Hint)
398+
}
399+
if strings.Contains(cfgErr.Hint, "config init") {
400+
t.Errorf("agent hint must not mention config init; got %q", cfgErr.Hint)
383401
}
384-
// Should suggest config bind, not config init
385-
assertExitError(t, err, output.ExitValidation, output.ErrDetail{
386-
Type: "openclaw",
387-
Message: "openclaw context detected but lark-cli not bound to openclaw workspace",
388-
Hint: "run: lark-cli config bind --source openclaw",
389-
})
390402
}
391403

392404
// ── Helper function tests (dotenv, brand, path resolution) ──

cmd/config/bind_warning_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package config
5+
6+
import (
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
12+
"github.com/larksuite/cli/internal/cmdutil"
13+
)
14+
15+
// runHermesBindWithIdentity boots a Hermes-shaped fake env, runs `config bind`
16+
// with the given identity preset in flag (non-TUI) mode, and returns captured
17+
// stderr. Hermes is the simplest source to fake (single .env file).
18+
func runHermesBindWithIdentity(t *testing.T, identity string) string {
19+
t.Helper()
20+
saveWorkspace(t)
21+
configDir := t.TempDir()
22+
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
23+
24+
hermesHome := t.TempDir()
25+
t.Setenv("HERMES_HOME", hermesHome)
26+
envContent := "FEISHU_APP_ID=cli_hermes_abc\nFEISHU_APP_SECRET=hermes_secret_123\nFEISHU_DOMAIN=lark\n"
27+
if err := os.WriteFile(filepath.Join(hermesHome, ".env"), []byte(envContent), 0600); err != nil {
28+
t.Fatalf("write .env: %v", err)
29+
}
30+
31+
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
32+
err := configBindRun(&BindOptions{
33+
Factory: f,
34+
Source: "hermes",
35+
Identity: identity,
36+
Lang: "zh",
37+
})
38+
if err != nil {
39+
t.Fatalf("bind failed: %v", err)
40+
}
41+
return stderr.String()
42+
}
43+
44+
// TestConfigBindRun_UserDefaultIdentity_WarnsAboutImpersonation covers the
45+
// gap that previously slipped through: a fresh flag-mode bind landing on
46+
// user-default. warnIdentityEscalation requires a previous bot lock to fire,
47+
// and IdentityUserDefaultDesc only renders in TUI selection — so without
48+
// noticeUserDefaultRisk the user/AI never see the impersonation risk on a
49+
// first-time user-default bind.
50+
func TestConfigBindRun_UserDefaultIdentity_WarnsAboutImpersonation(t *testing.T) {
51+
out := runHermesBindWithIdentity(t, "user-default")
52+
if !strings.Contains(out, bindMsgZh.IdentityEscalationMessage) {
53+
t.Errorf("user-default bind must surface IdentityEscalationMessage; got: %s", out)
54+
}
55+
}
56+
57+
func TestConfigBindRun_BotOnlyIdentity_NoImpersonationWarning(t *testing.T) {
58+
out := runHermesBindWithIdentity(t, "bot-only")
59+
if strings.Contains(out, bindMsgZh.IdentityEscalationMessage) {
60+
t.Errorf("bot-only bind must NOT warn about impersonation; got: %s", out)
61+
}
62+
}

cmd/config/config_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,15 @@ func TestConfigShowRun_NotConfiguredReturnsStructuredError(t *testing.T) {
9090
t.Fatal("expected error")
9191
}
9292

93-
var exitErr *output.ExitError
94-
if !errors.As(err, &exitErr) {
95-
t.Fatalf("error type = %T, want *output.ExitError", err)
93+
var cfgErr *core.ConfigError
94+
if !errors.As(err, &cfgErr) {
95+
t.Fatalf("error type = %T, want *core.ConfigError", err)
9696
}
97-
if exitErr.Code != output.ExitValidation {
98-
t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitValidation)
97+
if cfgErr.Code != output.ExitValidation {
98+
t.Fatalf("exit code = %d, want %d", cfgErr.Code, output.ExitValidation)
9999
}
100-
if exitErr.Detail == nil || exitErr.Detail.Type != "config" || exitErr.Detail.Message != "not configured" {
101-
t.Fatalf("detail = %#v, want config/not configured", exitErr.Detail)
100+
if cfgErr.Type != "config" || cfgErr.Message != "not configured" {
101+
t.Fatalf("detail = %+v, want config/not configured", cfgErr)
102102
}
103103
}
104104

0 commit comments

Comments
 (0)