mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Give each AI Agent (OpenClaw, Hermes) its own lark-cli workspace so
its Feishu calls don't overwrite the developer's local config or
collide with other Agents.
lark-cli config bind [--source openclaw|hermes] [--app-id <id>]
[--identity bot-only|user-default] [--force]
Key capabilities:
- Source auto-detected from OPENCLAW_* / HERMES_* env signals; config
written to ~/.lark-cli/<agent>/, isolated per Agent.
- Two identity presets: 'bot-only' (flag-mode default) and
'user-default'. Flag mode rejects silent bot→user escalation
without --force; TUI prompts are exempt.
- Agent-friendly stdout JSON with 'identity' + 'message' for
next-step branching.
- 'config show' and 'doctor' expose the bound 'workspace'.
- OpenClaw SecretRef resolution: plain / ${VAR} / file:+JSON Pointer
/ exec:.
364 lines
9.7 KiB
Go
364 lines
9.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package binding
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestAssertSecurePath_NonAbsolutePath(t *testing.T) {
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: "relative/path.txt",
|
|
Label: "test",
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for non-absolute path, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path must be absolute, got %q", "relative/path.txt")
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_FileDoesNotExist(t *testing.T) {
|
|
nonexistent := filepath.Join(t.TempDir(), "nonexistent.txt")
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: nonexistent,
|
|
Label: "test",
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for non-existent file, got nil")
|
|
}
|
|
wantPrefix := fmt.Sprintf("test: cannot stat %q: ", nonexistent)
|
|
if !strings.HasPrefix(err.Error(), wantPrefix) {
|
|
t.Errorf("error = %q, want prefix %q", err.Error(), wantPrefix)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_ValidAbsolutePath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "valid.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != p {
|
|
t.Errorf("got %q, want %q", got, p)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_WorldWritable_Rejected(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("permission tests not applicable on Windows")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "insecure.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
if err := os.Chmod(p, 0o666); err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: false,
|
|
AllowReadableByOthers: true, // only test writable check
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for world-writable file, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is world-writable (mode 0666)", p)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_AllowInsecurePath_Bypasses(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("permission tests not applicable on Windows")
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "insecure.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
if err := os.Chmod(p, 0o666); err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != p {
|
|
t.Errorf("got %q, want %q", got, p)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_DirectoryRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: dir,
|
|
Label: "test",
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for directory path, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is a directory, not a file", dir)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_GroupWritable_Rejected(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("permission tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "groupw.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Chmod(p, 0o620); err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: false,
|
|
AllowReadableByOthers: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for group-writable file, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is group-writable (mode 0620)", p)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_WorldReadable_Rejected(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("permission tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "worldr.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Chmod(p, 0o604); err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: false,
|
|
AllowReadableByOthers: false,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for world-readable file, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is world-readable (mode 0604)", p)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_AllowReadableByOthers_Passes(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("permission tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "readable.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := os.Chmod(p, 0o644); err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: false,
|
|
AllowReadableByOthers: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != p {
|
|
t.Errorf("got %q, want %q", got, p)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_OwnerUID_Valid(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("owner UID tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "owned.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
AllowInsecurePath: false,
|
|
AllowReadableByOthers: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != p {
|
|
t.Errorf("got %q, want %q", got, p)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_Symlink_Rejected(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("symlink tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "real.txt")
|
|
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
link := filepath.Join(dir, "link.txt")
|
|
if err := os.Symlink(target, link); err != nil {
|
|
t.Fatalf("symlink: %v", err)
|
|
}
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: link,
|
|
Label: "test",
|
|
AllowSymlinkPath: false,
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for symlink with AllowSymlinkPath=false, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is a symlink (not allowed)", link)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_Symlink_Allowed(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("symlink tests not applicable on Windows")
|
|
}
|
|
dir := t.TempDir()
|
|
target := filepath.Join(dir, "real.txt")
|
|
if err := os.WriteFile(target, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
link := filepath.Join(dir, "link.txt")
|
|
if err := os.Symlink(target, link); err != nil {
|
|
t.Fatalf("symlink: %v", err)
|
|
}
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: link,
|
|
Label: "test",
|
|
AllowSymlinkPath: true,
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// On macOS /var → /private/var, so compare resolved paths
|
|
wantResolved, err := filepath.EvalSymlinks(target)
|
|
if err != nil {
|
|
t.Fatalf("EvalSymlinks(target): %v", err)
|
|
}
|
|
if got != wantResolved {
|
|
t.Errorf("got %q, want resolved %q", got, wantResolved)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_TrustedDirs_ExactMatch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "file.txt")
|
|
if err := os.WriteFile(p, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: p,
|
|
Label: "test",
|
|
TrustedDirs: []string{p},
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != p {
|
|
t.Errorf("got %q, want %q", got, p)
|
|
}
|
|
}
|
|
|
|
func TestAssertSecurePath_TrustedDirs(t *testing.T) {
|
|
trustedDir := t.TempDir()
|
|
untrustedDir := t.TempDir()
|
|
|
|
trustedFile := filepath.Join(trustedDir, "secret.txt")
|
|
if err := os.WriteFile(trustedFile, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
|
|
untrustedFile := filepath.Join(untrustedDir, "secret.txt")
|
|
if err := os.WriteFile(untrustedFile, []byte("data"), 0o600); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
|
|
// File outside trusted dir should fail
|
|
_, err := AssertSecurePath(AuditParams{
|
|
TargetPath: untrustedFile,
|
|
Label: "test",
|
|
TrustedDirs: []string{trustedDir},
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for file outside trusted dir, got nil")
|
|
}
|
|
want := fmt.Sprintf("test: path %q is not inside any trusted directory", untrustedFile)
|
|
if err.Error() != want {
|
|
t.Errorf("error = %q, want %q", err.Error(), want)
|
|
}
|
|
|
|
// File inside trusted dir should pass
|
|
got, err := AssertSecurePath(AuditParams{
|
|
TargetPath: trustedFile,
|
|
Label: "test",
|
|
TrustedDirs: []string{trustedDir},
|
|
AllowInsecurePath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != trustedFile {
|
|
t.Errorf("got %q, want %q", got, trustedFile)
|
|
}
|
|
}
|