chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
)
|
||||
|
||||
// Workspace identifies a config isolation context.
|
||||
// Each non-local workspace maps to a subdirectory under the base config dir.
|
||||
type Workspace string
|
||||
|
||||
const (
|
||||
// WorkspaceLocal is the default workspace. GetConfigDir returns the base
|
||||
// config dir without any subdirectory — identical to pre-workspace behavior.
|
||||
WorkspaceLocal Workspace = ""
|
||||
|
||||
// WorkspaceOpenClaw activates when any OpenClaw-specific env signal is
|
||||
// present (see DetectWorkspaceFromEnv for the full list).
|
||||
WorkspaceOpenClaw Workspace = "openclaw"
|
||||
|
||||
// WorkspaceHermes activates when any Hermes-specific env signal is
|
||||
// present (see DetectWorkspaceFromEnv for the full list).
|
||||
WorkspaceHermes Workspace = "hermes"
|
||||
|
||||
// WorkspaceLarkChannel activates when LARK_CHANNEL == "1" is set by
|
||||
// lark-channel-bridge in subprocesses it spawns (e.g. claude). See
|
||||
// DetectWorkspaceFromEnv for the detection rule.
|
||||
WorkspaceLarkChannel Workspace = "lark-channel"
|
||||
)
|
||||
|
||||
// currentWorkspace holds the workspace for the current process invocation.
|
||||
// Set once during Factory initialization; config bind's RunE may re-set it
|
||||
// to the workspace being bound. Uses atomic.Value for goroutine safety
|
||||
// (background registry refresh reads GetRuntimeDir concurrently with the
|
||||
// Factory init that writes workspace).
|
||||
var currentWorkspace atomic.Value // stores Workspace; zero value → Load returns nil → treated as Local
|
||||
|
||||
// SetCurrentWorkspace sets the active workspace for this process.
|
||||
func SetCurrentWorkspace(ws Workspace) {
|
||||
currentWorkspace.Store(ws)
|
||||
}
|
||||
|
||||
// CurrentWorkspace returns the active workspace.
|
||||
// Returns WorkspaceLocal if not yet set (safe default, backward-compatible).
|
||||
func CurrentWorkspace() Workspace {
|
||||
v := currentWorkspace.Load()
|
||||
if v == nil {
|
||||
return WorkspaceLocal
|
||||
}
|
||||
return v.(Workspace)
|
||||
}
|
||||
|
||||
// Display returns the user-visible workspace label.
|
||||
// Used in config show, doctor, and error messages.
|
||||
func (w Workspace) Display() string {
|
||||
if w == WorkspaceLocal || w == "" {
|
||||
return "local"
|
||||
}
|
||||
return string(w)
|
||||
}
|
||||
|
||||
// IsLocal returns true if this is the default local workspace.
|
||||
func (w Workspace) IsLocal() bool {
|
||||
return w == WorkspaceLocal || w == ""
|
||||
}
|
||||
|
||||
// DetectWorkspaceFromEnv determines the workspace from process environment.
|
||||
//
|
||||
// Detection is signal-based, not credential-based: we look for environment
|
||||
// variables that the host Agent itself sets when launching a subprocess.
|
||||
// Generic FEISHU_APP_ID / FEISHU_APP_SECRET are intentionally NOT used —
|
||||
// any third-party Feishu script can set those, so they would cause
|
||||
// false-positive routing into a Hermes workspace.
|
||||
//
|
||||
// Priority:
|
||||
// 1. Any OpenClaw signal → WorkspaceOpenClaw
|
||||
// - OPENCLAW_CLI == "1": subprocess marker (added 2026-03-09 via
|
||||
// OpenClaw PR #41411). Most precise, but absent on older builds.
|
||||
// - OPENCLAW_HOME / OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH non-empty:
|
||||
// user-facing paths introduced with the 2026-01-30 rename. Detected
|
||||
// so that OpenClaw builds predating the subprocess marker — or
|
||||
// invocation paths that do not propagate the marker — still route
|
||||
// correctly.
|
||||
// 2. Any Hermes signal → WorkspaceHermes. All of the checked variables are
|
||||
// set by Hermes itself (hermes_cli/main.py, gateway/run.py). No
|
||||
// unrelated tool uses the HERMES_* namespace.
|
||||
// - HERMES_HOME: exported by the CLI at startup
|
||||
// - HERMES_QUIET == "1": exported by the gateway
|
||||
// - HERMES_EXEC_ASK == "1": exported by the gateway (paired w/ QUIET)
|
||||
// - HERMES_GATEWAY_TOKEN: injected into every gateway subprocess
|
||||
// - HERMES_SESSION_KEY: session identifier scoped to the current chat
|
||||
// 3. LARK_CHANNEL == "1" → WorkspaceLarkChannel. Set by lark-channel-bridge
|
||||
// when spawning subprocesses (e.g. claude). Single boolean marker —
|
||||
// mirrors the OPENCLAW_CLI / HERMES_QUIET style.
|
||||
// 4. Otherwise → WorkspaceLocal
|
||||
func DetectWorkspaceFromEnv(getenv func(string) string) Workspace {
|
||||
if getenv("OPENCLAW_CLI") == "1" ||
|
||||
getenv("OPENCLAW_HOME") != "" ||
|
||||
getenv("OPENCLAW_STATE_DIR") != "" ||
|
||||
getenv("OPENCLAW_CONFIG_PATH") != "" ||
|
||||
getenv("OPENCLAW_SERVICE_MARKER") != "" ||
|
||||
getenv("OPENCLAW_SERVICE_VERSION") != "" ||
|
||||
getenv("OPENCLAW_GATEWAY_PORT") != "" ||
|
||||
getenv("OPENCLAW_SHELL") != "" {
|
||||
return WorkspaceOpenClaw
|
||||
}
|
||||
if getenv("HERMES_HOME") != "" ||
|
||||
getenv("HERMES_QUIET") == "1" ||
|
||||
getenv("HERMES_EXEC_ASK") == "1" ||
|
||||
getenv("HERMES_GATEWAY_TOKEN") != "" ||
|
||||
getenv("HERMES_SESSION_KEY") != "" {
|
||||
return WorkspaceHermes
|
||||
}
|
||||
if getenv("LARK_CHANNEL") == "1" {
|
||||
return WorkspaceLarkChannel
|
||||
}
|
||||
return WorkspaceLocal
|
||||
}
|
||||
|
||||
// GetBaseConfigDir returns the root config directory, ignoring workspace.
|
||||
// Priority: LARKSUITE_CLI_CONFIG_DIR env → ~/.lark-cli.
|
||||
// If the home directory cannot be determined and no override is set, a
|
||||
// warning is written to stderr and the path falls back to a relative
|
||||
// ".lark-cli" — callers will then see an explicit I/O error at first use
|
||||
// instead of a silent misconfiguration.
|
||||
func GetBaseConfigDir() string {
|
||||
if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
home, err := vfs.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
// Fall back to a relative ".lark-cli" so the first I/O operation
|
||||
// surfaces a clear "no such file or directory" error. We cannot
|
||||
// emit a stderr warning here — this package has no IOStreams in
|
||||
// scope, and direct writes to os.Stderr violate the IOStreams
|
||||
// injection boundary (enforced by lint). Users who hit this path
|
||||
// should set LARKSUITE_CLI_CONFIG_DIR explicitly.
|
||||
home = ""
|
||||
}
|
||||
return filepath.Join(home, ".lark-cli")
|
||||
}
|
||||
|
||||
// GetRuntimeDir returns the workspace-aware config directory.
|
||||
// - WorkspaceLocal → GetBaseConfigDir() (unchanged, backward-compatible)
|
||||
// - WorkspaceOpenClaw → GetBaseConfigDir()/openclaw
|
||||
// - WorkspaceHermes → GetBaseConfigDir()/hermes
|
||||
// - WorkspaceLarkChannel → GetBaseConfigDir()/lark-channel
|
||||
func GetRuntimeDir() string {
|
||||
base := GetBaseConfigDir()
|
||||
ws := CurrentWorkspace()
|
||||
if ws.IsLocal() {
|
||||
return base
|
||||
}
|
||||
return filepath.Join(base, string(ws))
|
||||
}
|
||||
Reference in New Issue
Block a user