deer-flow/backend/tests/test_lark_cli_integration.py
Ryker_Feng aacb99cfd2
feat(lark): sidecar credential broker for sandbox lark-cli (Pattern B) (#4501)
* feat(lark): sidecar credential broker for sandbox lark-cli (Pattern B)

Removes the plaintext Lark credential mounts (appSecret + OAuth tokens)
from the sandbox container. A long-running broker sidecar owns lark-cli
and the per-user config/data dirs and serves the command surface over
Pod loopback; the sandbox gets only a forwarding shim on PATH, so the
raw credential files never exist in the sandbox filesystem.

- lark_broker.py: stdlib-only loopback broker (argv passthrough with
  shell=False, server-injected credential env, bounded I/O) + shim
  script constant + install-shim mode.
- docker/lark-cli-broker: init(install-shim) + serve image.
- provisioner: LARK_CLI_BROKER_IMAGE + provision_lark_cli_broker →
  shim init container + lark-cli-broker sidecar (config/data mounted
  sidecar-only); credentials dropped from the sandbox container;
  /api/capabilities reports lark_cli_broker_image. Broker supersedes
  the Pattern A init-container binary when both are configured.
- gateway: lark_cli_env_overlay(broker=True) omits config/data env;
  sandbox_lark_broker_active() TTL-cached mode resolver; broker added
  to sandbox_runtime_mode / readiness and the settings UI.

Opt-in and off by default (empty LARK_CLI_BROKER_IMAGE ⇒ no change).

Closes #4338

* fix(lark): address Pattern B broker review findings (#4501)

Follow-up to the sidecar credential broker addressing the PR #4501 review:

- shim: split the on-PATH lark-cli into a /bin/sh launcher + Python shim body
  so broker mode fails loudly (exit 127, actionable message) instead of ENOEXEC
  when the sandbox image ships no python3; interpreter pinnable via
  DEERFLOW_LARK_BROKER_PYTHON. Launcher bakes in the shim's absolute path since
  $0 is the bare command name when run off PATH.
- broker: drop the dead cwd payload field (broker can't see the sandbox FS) and
  document the command-surface-only / no-file-IO limitation.
- broker: return a structured 500 JSON on unexpected exec errors so the shim
  gets a meaningful message, not an opaque transport failure; set a handler
  socket timeout to bound slow/stuck connections.
- broker: add an opt-in DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS denylist that
  refuses secret-dumping subcommands before spawning the binary, forwarded from
  the provisioner sidecar.
- gateway: tighten the per-bash-call broker probe timeout (1.5s) and cache
  negatives longer (300s) so non-broker remote-provisioner users don't pay a
  latency hit; guard the mode cache with a lock; drop the dead
  _probe_provisioner_lark_cli_init_image wrapper.
- docs: remove the broken design-doc link from the broker README.

Adds tests for launcher python resolution, cwd omission, denylist enforcement,
500-on-error, hot-path probe timeout + negative caching, and provisioner
denylist-env wiring.
2026-07-28 22:54:44 +08:00

1683 lines
67 KiB
Python

from __future__ import annotations
import hashlib
import inspect
import io
import json
import multiprocessing
import re
import shutil
import stat
import subprocess
import tarfile
import threading
import time
import zipfile
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import pytest
from _router_auth_helpers import make_authed_test_app
from fastapi.testclient import TestClient
from app.gateway.auth.models import User
from app.gateway.deps import get_config
from app.gateway.routers import integrations as integrations_router
from deerflow.config import paths as paths_module
from deerflow.config.paths import Paths
from deerflow.integrations import lark_cli
from deerflow.sandbox.tools import _lark_cli_env_from_runtime
from deerflow.skills.storage import reset_skill_storage
from deerflow.skills.storage.user_scoped_skill_storage import UserScopedSkillStorage
from deerflow.skills.types import SkillCategory
def _skill_content(name: str) -> str:
return f"---\nname: {name}\ndescription: {name} integration skill\n---\n\n# {name}\n"
def _make_lark_cli_source_zip(tmp_path: Path, *, omit_skill: str | None = None, renamed_skill: str | None = None) -> Path:
archive = tmp_path / "lark-cli.zip"
with zipfile.ZipFile(archive, "w") as zf:
for skill_name in lark_cli.LARK_SKILL_NAMES:
if skill_name == omit_skill:
continue
declared_name = f"{skill_name}-renamed" if skill_name == renamed_skill else skill_name
zf.writestr(f"cli-1.0.65/skills/{skill_name}/SKILL.md", _skill_content(declared_name))
zf.writestr(f"cli-1.0.65/skills/{skill_name}/references/readme.md", f"# {skill_name}\n")
return archive
def _make_lark_cli_binary_tar(payload: bytes, *, member_name: str = "lark-cli") -> bytes:
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:gz") as tf:
info = tarfile.TarInfo(member_name)
info.mode = 0o755
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
return buffer.getvalue()
def _assert_lark_root_missing(user_id: str) -> None:
root = lark_cli.lark_integration_root(user_id)
assert not root.exists()
def _config(skills_root: Path):
return SimpleNamespace(
skills=SimpleNamespace(
get_skills_path=lambda: skills_root,
container_path="/mnt/skills",
use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage",
)
)
def _patch_paths(monkeypatch, base_dir: Path) -> None:
monkeypatch.setattr(paths_module, "_paths", Paths(base_dir=base_dir))
def test_sandbox_lark_cli_env_prepends_managed_linux_runtime() -> None:
overlay = lark_cli.lark_cli_env_overlay("alice", sandbox_paths=True)
assert overlay["PATH"].split(":", 1)[0] == "/mnt/integrations/lark-cli/runtime/bin"
assert overlay["LARKSUITE_CLI_CONFIG_DIR"] == "/mnt/integrations/lark-cli/config"
assert overlay["LARKSUITE_CLI_DATA_DIR"] == "/mnt/integrations/lark-cli/data"
def test_init_image_launcher_matches_python_constant() -> None:
"""The init image's build script must embed the same launcher as the Gateway.
Both produce ``bin/lark-cli``; if they drift, the sandbox PATH contract
breaks for one provisioning mode.
"""
repo_root = Path(lark_cli.__file__).resolve().parents[5]
build_script = repo_root / "docker" / "lark-cli-init" / "build-runtime.sh"
assert build_script.is_file(), f"missing init image build script at {build_script}"
body = build_script.read_text(encoding="utf-8")
# The launcher heredoc in the build script must contain the exact script body.
assert lark_cli.LARK_CLI_SANDBOX_LAUNCHER_SCRIPT.strip() in body
def test_managed_sandbox_runtime_verifies_and_installs_linux_archives(monkeypatch, tmp_path) -> None:
assert hasattr(lark_cli, "_ensure_managed_sandbox_lark_cli"), "managed sandbox runtime installer is missing"
_patch_paths(monkeypatch, tmp_path / "home")
archives = {
"lark-cli-1.0.65-linux-amd64.tar.gz": _make_lark_cli_binary_tar(b"amd64-binary"),
"lark-cli-1.0.65-linux-arm64.tar.gz": _make_lark_cli_binary_tar(b"arm64-binary"),
}
checksums = "".join(f"{hashlib.sha256(payload).hexdigest()} {name}\n" for name, payload in archives.items()).encode()
assets = {"checksums.txt": checksums, **archives}
monkeypatch.setattr(lark_cli, "_download_lark_release_asset", lambda _version, name, **_kwargs: assets[name])
runtime = lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"amd64-binary"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"arm64-binary"
assert stat.S_IMODE((runtime / "linux-amd64" / "lark-cli").stat().st_mode) == 0o755
launcher = (runtime / "bin" / "lark-cli").read_text(encoding="utf-8")
assert "uname -m" in launcher
assert "x86_64" in launcher and "aarch64" in launcher
def test_managed_sandbox_runtime_rejects_checksum_mismatch(monkeypatch, tmp_path) -> None:
assert hasattr(lark_cli, "_ensure_managed_sandbox_lark_cli"), "managed sandbox runtime installer is missing"
_patch_paths(monkeypatch, tmp_path / "home")
archives = {
"lark-cli-1.0.65-linux-amd64.tar.gz": _make_lark_cli_binary_tar(b"amd64-binary"),
"lark-cli-1.0.65-linux-arm64.tar.gz": _make_lark_cli_binary_tar(b"arm64-binary"),
}
bad_checksums = "".join(f"{'0' * 64} {name}\n" for name in archives).encode()
assets = {"checksums.txt": bad_checksums, **archives}
monkeypatch.setattr(lark_cli, "_download_lark_release_asset", lambda _version, name, **_kwargs: assets[name])
with pytest.raises(ValueError, match="checksum"):
lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert not lark_cli.lark_cli_managed_sandbox_dir().exists()
def test_managed_sandbox_runtime_rejects_unsafe_tar_member(monkeypatch, tmp_path) -> None:
assert hasattr(lark_cli, "_ensure_managed_sandbox_lark_cli"), "managed sandbox runtime installer is missing"
_patch_paths(monkeypatch, tmp_path / "home")
unsafe = _make_lark_cli_binary_tar(b"binary", member_name="../lark-cli")
safe = _make_lark_cli_binary_tar(b"binary")
archives = {
"lark-cli-1.0.65-linux-amd64.tar.gz": unsafe,
"lark-cli-1.0.65-linux-arm64.tar.gz": safe,
}
checksums = "".join(f"{hashlib.sha256(payload).hexdigest()} {name}\n" for name, payload in archives.items()).encode()
assets = {"checksums.txt": checksums, **archives}
monkeypatch.setattr(lark_cli, "_download_lark_release_asset", lambda _version, name, **_kwargs: assets[name])
with pytest.raises(ValueError, match="Unsafe Lark CLI runtime archive member"):
lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
def test_managed_sandbox_runtime_accepts_prestaged_airgapped_tree(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
launcher.chmod(0o755)
monkeypatch.setenv(lark_cli.LARK_CLI_SANDBOX_RUNTIME_SOURCE_ENV, str(source))
monkeypatch.setattr(
lark_cli,
"_download_lark_release_asset",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("air-gapped install must not download")),
)
runtime = lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert (runtime / "linux-amd64" / "lark-cli").read_bytes() == b"amd64-binary"
assert (runtime / "linux-arm64" / "lark-cli").read_bytes() == b"arm64-binary"
def test_managed_sandbox_runtime_rejects_any_symlink_in_prestaged_tree(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
launcher.chmod(0o755)
outside = tmp_path / "outside-secret"
outside.write_text("must-not-be-copied", encoding="utf-8")
try:
(source / "extra-link").symlink_to(outside)
except OSError as exc:
pytest.skip(f"symlinks are not available: {exc}")
monkeypatch.setenv(lark_cli.LARK_CLI_SANDBOX_RUNTIME_SOURCE_ENV, str(source))
with pytest.raises(ValueError, match="symlink"):
lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert not lark_cli.lark_cli_managed_sandbox_dir().exists()
def test_managed_sandbox_runtime_rejects_non_executable_prestaged_binary(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.chmod(0o755)
(source / "linux-arm64" / "lark-cli").chmod(0o644)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
launcher.chmod(0o755)
monkeypatch.setenv(lark_cli.LARK_CLI_SANDBOX_RUNTIME_SOURCE_ENV, str(source))
with pytest.raises(ValueError, match="executable"):
lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
assert not lark_cli.lark_cli_managed_sandbox_dir().exists()
def test_concurrent_managed_sandbox_runtime_installs_serialize_replacement(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
source = tmp_path / "pre-staged"
for arch in ("amd64", "arm64"):
binary = source / f"linux-{arch}" / "lark-cli"
binary.parent.mkdir(parents=True)
binary.write_bytes(f"{arch}-binary".encode())
binary.chmod(0o755)
launcher = source / "bin" / "lark-cli"
launcher.parent.mkdir(parents=True)
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
launcher.chmod(0o755)
monkeypatch.setenv(lark_cli.LARK_CLI_SANDBOX_RUNTIME_SOURCE_ENV, str(source))
real_validate = lark_cli._validate_lark_cli_sandbox_runtime
start = threading.Barrier(2)
state_lock = threading.Lock()
active = 0
max_active = 0
def _slow_validate(root):
nonlocal active, max_active
with state_lock:
active += 1
max_active = max(max_active, active)
try:
time.sleep(0.15)
return real_validate(root)
finally:
with state_lock:
active -= 1
def _install():
start.wait()
return lark_cli._ensure_managed_sandbox_lark_cli("v1.0.65")
monkeypatch.setattr(lark_cli, "_validate_lark_cli_sandbox_runtime", _slow_validate)
with ThreadPoolExecutor(max_workers=2) as pool:
results = [future.result(timeout=5) for future in [pool.submit(_install) for _ in range(2)]]
assert results[0] == results[1] == lark_cli.lark_cli_managed_sandbox_dir()
assert max_active == 1
assert not list(results[0].parent.glob(".replacing-sandbox-cli-*"))
def test_install_lark_integration_installs_one_readonly_pack_for_all_users(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
result = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert result.success is True
assert "lark-doc" in result.installed_skills
assert result.status.installed is True
root = lark_cli.lark_integration_root("alice")
assert root == lark_cli.lark_integration_root("bob")
assert root == tmp_path / "home" / "integrations" / "skills" / "lark-cli"
assert (root / "lark-doc" / "SKILL.md").is_file()
assert (root / lark_cli.LARK_CLI_MANIFEST_FILE).is_file()
shared_content = (root / "lark-shared" / "SKILL.md").read_text(encoding="utf-8")
assert "?settings=integrations" in shared_content
assert "不要要求用户在终端执行" in shared_content
assert "Exact OAuth scope" in shared_content
storage = UserScopedSkillStorage("alice", host_path=str(skills_root), app_config=config)
skills = storage.load_skills(enabled_only=False)
lark_doc = next(skill for skill in skills if skill.name == "lark-doc")
assert lark_doc.category == SkillCategory.INTEGRATION
assert lark_doc.get_container_file_path("/mnt/skills") == "/mnt/skills/integrations/lark-cli/lark-doc/SKILL.md"
assert lark_doc.enabled is True
bob_storage = UserScopedSkillStorage("bob", host_path=str(skills_root), app_config=config)
bob_lark_doc = next(skill for skill in bob_storage.load_skills(enabled_only=False) if skill.name == "lark-doc")
assert bob_lark_doc.category == SkillCategory.INTEGRATION
assert bob_lark_doc.skill_file == root / "lark-doc" / "SKILL.md"
reset_skill_storage()
def test_aio_install_provisions_matching_linux_sandbox_runtime(monkeypatch, tmp_path) -> None:
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
config.sandbox = SimpleNamespace(use="deerflow.community.aio_sandbox:AioSandboxProvider")
archive = _make_lark_cli_source_zip(tmp_path)
provisioned_versions: list[str] = []
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
monkeypatch.setattr(
lark_cli,
"_ensure_managed_sandbox_lark_cli",
lambda version: provisioned_versions.append(version) or lark_cli.lark_cli_managed_sandbox_dir(),
)
result = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert result.success is True
assert provisioned_versions == ["v1.0.65"]
reset_skill_storage()
def test_remote_provisioner_install_skips_gateway_sandbox_runtime(monkeypatch, tmp_path) -> None:
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
archive = _make_lark_cli_source_zip(tmp_path)
provisioned_versions: list[str] = []
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
monkeypatch.setattr(
lark_cli,
"_ensure_managed_sandbox_lark_cli",
lambda version: provisioned_versions.append(version) or lark_cli.lark_cli_managed_sandbox_dir(),
)
result = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert result.success is True
# Remote provisioner mode gets the runtime from an init container, so the
# Gateway must not download Linux binaries at install time.
assert provisioned_versions == []
reset_skill_storage()
def test_status_runtime_mode_none_for_non_aio(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(use="deerflow.sandbox.local:LocalSandboxProvider")
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "none"
assert ready is False
assert detail
def test_status_runtime_mode_gateway_download_ready(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(use="deerflow.community.aio_sandbox:AioSandboxProvider")
# Stage a valid runtime dir so validation passes.
runtime = lark_cli.lark_cli_managed_sandbox_dir()
(runtime / "bin").mkdir(parents=True)
(runtime / "bin" / "lark-cli").write_text("#!/bin/sh\n", encoding="utf-8")
(runtime / "bin" / "lark-cli").chmod(0o755)
for arch in lark_cli.LARK_CLI_LINUX_ARCHES:
(runtime / f"linux-{arch}").mkdir(parents=True)
target = runtime / f"linux-{arch}" / "lark-cli"
target.write_bytes(b"\x7fELF")
target.chmod(0o755)
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "gateway-download"
assert ready is True
assert detail is None
def test_status_runtime_mode_gateway_download_not_ready(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(use="deerflow.community.aio_sandbox:AioSandboxProvider")
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "gateway-download"
assert ready is False
assert detail
def test_status_runtime_mode_init_container_ready(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", lambda _config: {"lark_cli_init_image": True, "lark_cli_broker_image": False})
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "init-container"
assert ready is True
assert detail is None
def test_status_runtime_mode_broker_supersedes_init_container(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
# Broker (Pattern B) wins even when the init image is also configured.
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", lambda _config: {"lark_cli_init_image": True, "lark_cli_broker_image": True})
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "broker"
assert ready is True
assert detail is None
def test_status_runtime_mode_init_container_not_configured(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", lambda _config: {"lark_cli_init_image": False, "lark_cli_broker_image": False})
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "init-container"
assert ready is False
assert detail
def test_status_runtime_mode_init_container_unreachable(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", lambda _config: None)
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=True)
assert mode == "init-container"
assert ready is False
assert detail
def test_status_runtime_probe_skipped_when_not_requested(monkeypatch, tmp_path) -> None:
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
def _fail(_config): # pragma: no cover - must not be called
raise AssertionError("provisioner should not be probed when probe=False")
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", _fail)
mode, ready, detail = lark_cli._resolve_sandbox_runtime_readiness(config, probe=False)
assert mode == "init-container"
assert ready is False
assert detail is None
def _reset_broker_mode_cache() -> None:
if hasattr(lark_cli.sandbox_lark_broker_active, "_cache"):
del lark_cli.sandbox_lark_broker_active._cache
def test_sandbox_lark_broker_active_uses_tight_hot_path_timeout(monkeypatch, tmp_path) -> None:
"""The per-bash-call broker probe must use the tight hot-path timeout, not the
5s Settings-status budget, so non-broker remote-provisioner users don't pay a
multi-second latency hit on the first lark-cli call per TTL."""
_reset_broker_mode_cache()
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
seen: dict[str, float] = {}
def _capture(_config, *, timeout):
seen["timeout"] = timeout
return {"lark_cli_init_image": False, "lark_cli_broker_image": True}
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", _capture)
try:
assert lark_cli.sandbox_lark_broker_active(config) is True
assert seen["timeout"] == lark_cli.LARK_BROKER_MODE_PROBE_TIMEOUT_SECONDS
finally:
_reset_broker_mode_cache()
def test_sandbox_lark_broker_active_caches_negative_result(monkeypatch, tmp_path) -> None:
"""A non-broker result is cached (longer TTL) so the hot path stops probing."""
_reset_broker_mode_cache()
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(
use="deerflow.community.aio_sandbox:AioSandboxProvider",
provisioner_url="http://provisioner:8002",
)
calls = {"n": 0}
def _probe(_config, *, timeout):
calls["n"] += 1
return {"lark_cli_init_image": True, "lark_cli_broker_image": False}
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", _probe)
try:
assert lark_cli.sandbox_lark_broker_active(config) is False
assert lark_cli.sandbox_lark_broker_active(config) is False
# Second call served from cache — the provisioner is probed only once.
assert calls["n"] == 1
finally:
_reset_broker_mode_cache()
def test_sandbox_lark_broker_active_false_without_remote_provisioner(monkeypatch, tmp_path) -> None:
"""Local AIO (no provisioner URL) never probes and is never broker mode."""
_reset_broker_mode_cache()
config = _config(tmp_path / "skills")
config.sandbox = SimpleNamespace(use="deerflow.community.aio_sandbox:AioSandboxProvider")
def _fail(_config, *, timeout): # pragma: no cover - must not be called
raise AssertionError("no provisioner should be probed without a provisioner_url")
monkeypatch.setattr(lark_cli, "_probe_provisioner_capabilities", _fail)
try:
assert lark_cli.sandbox_lark_broker_active(config) is False
finally:
_reset_broker_mode_cache()
def test_install_lark_integration_is_idempotent_across_reinstalls(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
first = lark_cli.install_lark_integration("alice", config, source_archive=archive)
root = lark_cli.lark_integration_root("alice")
# Drop a stray file so the reinstall must replace the whole tree, not merge.
stray = root / "lark-doc" / "stray.txt"
stray.write_text("stale", encoding="utf-8")
second = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert first.installed_skills == second.installed_skills
assert second.status.installed is True
assert (root / "lark-doc" / "SKILL.md").is_file()
assert not stray.exists()
# No leftover backup/staging dirs beside the target after a reinstall.
parent = root.parent
leftovers = [p.name for p in parent.iterdir() if p.name not in {lark_cli.INTEGRATION_ID, ".lark-cli.install.lock"}]
assert leftovers == []
reset_skill_storage()
@pytest.mark.parametrize("_attempt", range(5))
def test_concurrent_lark_skill_reinstalls_serialize_atomic_replacement(monkeypatch, tmp_path, _attempt) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
archive = _make_lark_cli_source_zip(tmp_path)
real_extract = lark_cli._extract_lark_skills
start = threading.Barrier(2)
state_lock = threading.Lock()
active = 0
max_active = 0
def _slow_extract(zf, destination):
nonlocal active, max_active
with state_lock:
active += 1
max_active = max(max_active, active)
try:
time.sleep(0.15)
return real_extract(zf, destination)
finally:
with state_lock:
active -= 1
def _install():
start.wait()
return lark_cli._install_lark_skills_from_archive("alice", archive, version="v1.0.65")
monkeypatch.setattr(lark_cli, "_extract_lark_skills", _slow_extract)
with ThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(_install) for _ in range(2)]
results = [future.result(timeout=5) for future in futures]
assert results[0] == results[1]
assert max_active == 1
root = lark_cli.lark_integration_root()
assert (root / "lark-doc" / "SKILL.md").is_file()
assert not list(root.parent.glob(".replacing-lark-cli-*"))
@pytest.mark.skipif(
"fork" not in multiprocessing.get_all_start_methods() or lark_cli.fcntl is None,
reason="requires POSIX fork and fcntl",
)
def test_concurrent_lark_skill_reinstalls_serialize_across_processes(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
archive = _make_lark_cli_source_zip(tmp_path)
real_extract = lark_cli._extract_lark_skills
context = multiprocessing.get_context("fork")
start = context.Barrier(2)
active = context.Value("i", 0)
max_active = context.Value("i", 0)
results = context.Queue()
def _slow_extract(zf, destination):
with active.get_lock(), max_active.get_lock():
active.value += 1
max_active.value = max(max_active.value, active.value)
try:
time.sleep(0.2)
return real_extract(zf, destination)
finally:
with active.get_lock():
active.value -= 1
def _install():
try:
start.wait(timeout=5)
installed, digest = lark_cli._install_lark_skills_from_archive("alice", archive, version="v1.0.65")
results.put((installed, digest, None))
except BaseException as exc: # noqa: BLE001 - propagate child failure
results.put((None, None, repr(exc)))
monkeypatch.setattr(lark_cli, "_extract_lark_skills", _slow_extract)
processes = [context.Process(target=_install) for _ in range(2)]
for process in processes:
process.start()
for process in processes:
process.join(timeout=10)
assert [process.exitcode for process in processes] == [0, 0]
child_results = [results.get(timeout=2) for _ in processes]
assert all(error is None for _installed, _digest, error in child_results), child_results
assert child_results[0][:2] == child_results[1][:2]
assert max_active.value == 1
assert not list(lark_cli.lark_integration_root().parent.glob(".replacing-lark-cli-*"))
def test_install_lark_integration_succeeds_when_backup_cleanup_fails(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
# First install lays down the target so the reinstall has a backup to clean.
lark_cli.install_lark_integration("alice", config, source_archive=archive)
real_rmtree = shutil.rmtree
forced_raises = {"count": 0}
def _rmtree(path, *args, **kwargs):
# The post-rename backup deletion is best-effort and now passes
# ignore_errors=True, so a transient FS error there must not flip a
# successful install into a failure. Force any rmtree that does *not*
# ignore errors to raise, proving the success path no longer depends on
# a fragile backup cleanup.
if kwargs.get("ignore_errors"):
return real_rmtree(path, *args, **kwargs)
forced_raises["count"] += 1
raise OSError("transient FS error during backup cleanup")
monkeypatch.setattr(lark_cli.shutil, "rmtree", _rmtree)
result = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert result.success is True
root = lark_cli.lark_integration_root("alice")
assert (root / "lark-doc" / "SKILL.md").is_file()
# No non-ignoring rmtree is relied upon on the success path, and no leftover
# backup dir remains beside the target after the reinstall.
assert forced_raises["count"] == 0
leftovers = [p.name for p in root.parent.iterdir() if p.name not in {lark_cli.INTEGRATION_ID, ".lark-cli.install.lock"}]
assert leftovers == []
reset_skill_storage()
def test_install_lark_integration_records_content_sha_in_manifest(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
lark_cli.install_lark_integration("alice", config, source_archive=archive)
manifest = json.loads((lark_cli.lark_integration_root("alice") / lark_cli.LARK_CLI_MANIFEST_FILE).read_text(encoding="utf-8"))
assert manifest["version"] == "v1.0.65"
assert isinstance(manifest["content_sha256"], str)
assert len(manifest["content_sha256"]) == 64
reset_skill_storage()
def test_install_lark_integration_reports_content_change_on_reinstall(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "probe_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="v1.0.65"))
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
first = lark_cli.install_lark_integration("alice", config, source_archive=archive)
assert "content changed" not in first.message
changed_dir = tmp_path / "changed"
changed_dir.mkdir()
changed_archive = _make_lark_cli_source_zip(changed_dir)
with zipfile.ZipFile(changed_archive, "a") as zf:
zf.writestr("cli-1.0.65/skills/lark-doc/references/extra.md", "# extra content\n")
second = lark_cli.install_lark_integration("alice", config, source_archive=changed_archive)
assert "content changed" in second.message
reset_skill_storage()
def test_install_lark_integration_rejects_zip_slip_member(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path)
with zipfile.ZipFile(archive, "a") as zf:
zf.writestr("../evil.txt", "escape")
with pytest.raises(ValueError, match="Unsafe Lark CLI archive member"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_install_lark_integration_rejects_symlink_member(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path)
link_info = zipfile.ZipInfo("cli-1.0.65/skills/lark-doc/references/link")
link_info.external_attr = (stat.S_IFLNK | 0o777) << 16
with zipfile.ZipFile(archive, "a") as zf:
zf.writestr(link_info, "target")
with pytest.raises(ValueError, match="Unsafe Lark CLI archive member"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_install_lark_integration_rejects_executable_binary_member(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path)
with zipfile.ZipFile(archive, "a") as zf:
zf.writestr("cli-1.0.65/skills/lark-doc/bin/tool", b"\x7fELFbinary")
with pytest.raises(ValueError, match="executable binary member"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_install_lark_integration_rejects_oversized_extraction(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path)
monkeypatch.setattr(lark_cli, "LARK_CLI_MAX_EXTRACTED_BYTES", 128)
with pytest.raises(ValueError, match="expands to too much data"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_install_lark_integration_rejects_missing_required_skill(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path, omit_skill="lark-doc")
with pytest.raises(ValueError, match="missing required skills: lark-doc"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_install_lark_integration_rejects_renamed_skill_metadata(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
archive = _make_lark_cli_source_zip(tmp_path, renamed_skill="lark-doc")
with pytest.raises(ValueError, match="declares name 'lark-doc-renamed'"):
lark_cli.install_lark_integration("alice", config, source_archive=archive)
_assert_lark_root_missing("alice")
def test_fallback_and_docker_lark_cli_versions_match():
dockerfile = Path(__file__).resolve().parents[1] / "Dockerfile"
match = re.search(r"^ARG LARK_CLI_NPM_VERSION=(?P<version>\S+)$", dockerfile.read_text(encoding="utf-8"), re.MULTILINE)
assert match is not None
assert lark_cli.LARK_CLI_NPM_VERSION == match.group("version")
assert lark_cli.FALLBACK_LARK_CLI_VERSION == f"v{lark_cli.LARK_CLI_NPM_VERSION}"
def test_resolve_lark_cli_path_prefers_managed_gateway_cli(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
managed_bin = lark_cli.lark_cli_managed_gateway_dir() / "node_modules" / ".bin" / "lark-cli"
managed_bin.parent.mkdir(parents=True)
managed_bin.write_text("#!/bin/sh\n", encoding="utf-8")
monkeypatch.setattr(lark_cli.shutil, "which", lambda _name: "/usr/bin/lark-cli")
assert lark_cli._resolve_lark_cli_path() == str(managed_bin)
def test_install_managed_gateway_lark_cli_uses_deerflow_prefix(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
captured: dict[str, object] = {}
monkeypatch.setattr(lark_cli.shutil, "which", lambda name: "/usr/bin/npm" if name == "npm" else None)
def _run(args, **kwargs):
captured["args"] = args
captured["kwargs"] = kwargs
managed_bin = lark_cli.lark_cli_managed_gateway_dir() / "node_modules" / ".bin" / "lark-cli"
managed_bin.parent.mkdir(parents=True)
managed_bin.write_text("#!/bin/sh\n", encoding="utf-8")
return subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
monkeypatch.setattr(lark_cli, "_probe_lark_cli_at_path", lambda path: lark_cli.LarkCliProbe(available=True, path=path, version="lark-cli VERSION 1.2.3"))
result = lark_cli._install_managed_gateway_lark_cli("v1.2.3")
assert result.available is True
assert result.version == "lark-cli VERSION 1.2.3"
assert captured["args"] == [
"/usr/bin/npm",
"install",
"--prefix",
str(lark_cli.lark_cli_managed_gateway_dir()),
"--no-audit",
"--no-fund",
"@larksuite/cli@1.2.3",
]
def test_install_lark_integration_installs_managed_gateway_cli_before_skill_pack(monkeypatch, tmp_path):
reset_skill_storage()
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
archive = _make_lark_cli_source_zip(tmp_path)
downloaded_versions: list[str] = []
monkeypatch.setattr(lark_cli, "probe_lark_auth", lambda _user_id, **_kwargs: lark_cli.LarkAuthProbe(status="not_configured", message="not configured"))
monkeypatch.setattr(lark_cli, "_ensure_managed_gateway_lark_cli", lambda: lark_cli.LarkCliProbe(available=True, path="/managed/bin/lark-cli", version="lark-cli VERSION 9.9.9"))
def _download(version: str) -> Path:
downloaded_versions.append(version)
return archive
monkeypatch.setattr(lark_cli, "_download_lark_archive", _download)
result = lark_cli.install_lark_integration("alice", config)
assert downloaded_versions == ["v9.9.9"]
assert result.status.manifest_version == "v9.9.9"
reset_skill_storage()
def test_resolve_latest_lark_cli_version_uses_release_tag(monkeypatch):
class _Resp:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return json.dumps({"tag_name": "v1.2.3"}).encode("utf-8")
monkeypatch.setattr(lark_cli.urllib.request, "urlopen", lambda *a, **k: _Resp())
assert lark_cli._resolve_latest_lark_cli_version() == "v1.2.3"
def test_resolve_latest_lark_cli_version_falls_back_on_error(monkeypatch):
def _boom(*a, **k):
raise OSError("network down")
monkeypatch.setattr(lark_cli.urllib.request, "urlopen", _boom)
assert lark_cli._resolve_latest_lark_cli_version() == lark_cli.FALLBACK_LARK_CLI_VERSION
def test_lark_archive_url_rejects_invalid_version_tag():
with pytest.raises(ValueError, match="Invalid Lark CLI version tag"):
lark_cli._lark_archive_url("v1.2.3/../../evil")
def test_start_lark_auth_returns_browser_url(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
captured: dict[str, object] = {}
def _run(args, **kwargs):
captured["args"] = args
captured["env"] = kwargs["env"]
return subprocess.CompletedProcess(
args=args,
returncode=0,
stdout=json.dumps(
{
"verification_url": "https://open.feishu.cn/auth/mock",
"device_code": "device-code",
"expires_in": 600,
}
),
stderr="",
)
monkeypatch.setattr(lark_cli.shutil, "which", lambda _name: "/usr/bin/lark-cli")
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
result = lark_cli.start_lark_auth("alice", domains=("calendar",), recommend=True)
assert result.verification_url == "https://open.feishu.cn/auth/mock"
assert result.device_code == "device-code"
assert captured["args"] == [
"/usr/bin/lark-cli",
"auth",
"login",
"--no-wait",
"--json",
"--recommend",
"--domain",
"calendar",
]
env = captured["env"]
assert isinstance(env, dict)
assert env["LARKSUITE_CLI_CONFIG_DIR"].endswith("users/alice/integrations/lark-cli/config")
def test_start_lark_auth_uses_minimal_login_by_default(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
captured: dict[str, object] = {}
def _run(args, **kwargs):
captured["args"] = args
captured["env"] = kwargs["env"]
return subprocess.CompletedProcess(
args=args,
returncode=0,
stdout=json.dumps(
{
"verification_url": "https://open.feishu.cn/auth/mock",
"device_code": "device-code",
"expires_in": 600,
}
),
stderr="",
)
monkeypatch.setattr(lark_cli.shutil, "which", lambda _name: "/usr/bin/lark-cli")
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
result = lark_cli.start_lark_auth("alice")
assert result.verification_url == "https://open.feishu.cn/auth/mock"
assert captured["args"] == [
"/usr/bin/lark-cli",
"auth",
"login",
"--no-wait",
"--json",
]
def test_lark_cli_env_from_runtime_exposes_settings_auth_to_lark_commands(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
runtime = SimpleNamespace(context={"user_id": "alice"})
env = _lark_cli_env_from_runtime(runtime, "lark-cli auth status --json", sandbox_paths=False)
assert env is not None
assert env["LARKSUITE_CLI_CONFIG_DIR"].endswith("users/alice/integrations/lark-cli/config")
assert env["LARKSUITE_CLI_DATA_DIR"].endswith("users/alice/integrations/lark-cli/data")
def test_lark_cli_env_hardens_existing_credential_tree(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
config_dir = lark_cli.lark_cli_config_dir("alice")
data_dir = lark_cli.lark_cli_data_dir("alice")
config_dir.mkdir(parents=True)
data_dir.mkdir(parents=True)
secret_file = config_dir / "config.json"
token_file = data_dir / "auth.json"
secret_file.write_text('{"appSecret":"secret"}', encoding="utf-8")
token_file.write_text('{"token":"secret"}', encoding="utf-8")
config_dir.chmod(0o755)
data_dir.chmod(0o777)
secret_file.chmod(0o644)
token_file.chmod(0o666)
lark_cli.lark_cli_env_overlay("alice")
assert stat.S_IMODE(config_dir.stat().st_mode) == 0o700
assert stat.S_IMODE(data_dir.stat().st_mode) == 0o700
assert stat.S_IMODE(secret_file.stat().st_mode) == 0o600
assert stat.S_IMODE(token_file.stat().st_mode) == 0o600
def test_lark_cli_env_rejects_symlinks_in_credential_tree(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
config_dir = lark_cli.lark_cli_config_dir("alice")
config_dir.mkdir(parents=True)
outside = tmp_path / "outside-secret"
outside.write_text("secret", encoding="utf-8")
try:
(config_dir / "config.json").symlink_to(outside)
except (NotImplementedError, OSError) as exc:
pytest.skip(f"symlinks are not available: {exc}")
with pytest.raises(ValueError, match="symlink"):
lark_cli.lark_cli_env_overlay("alice")
def test_save_lark_app_config_rehardens_files_written_by_cli(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
monkeypatch.setattr(lark_cli, "_require_lark_cli_path", lambda: "/usr/bin/lark-cli")
def _run(args, **kwargs):
config_file = Path(kwargs["env"]["LARKSUITE_CLI_CONFIG_DIR"]) / "config.json"
config_file.write_text('{"appSecret":"secret"}', encoding="utf-8")
config_file.chmod(0o644)
return subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
lark_cli._save_lark_app_config_with_cli("alice", app_id="cli_app", app_secret="secret", brand="feishu")
config_file = lark_cli.lark_cli_config_dir("alice") / "config.json"
assert stat.S_IMODE(config_file.stat().st_mode) == 0o600
def test_lark_cli_json_rehardens_auth_files_written_by_cli(monkeypatch, tmp_path) -> None:
_patch_paths(monkeypatch, tmp_path / "home")
def _run(args, **kwargs):
token_file = Path(kwargs["env"]["LARKSUITE_CLI_DATA_DIR"]) / "auth.json"
token_file.write_text('{"token":"secret"}', encoding="utf-8")
token_file.chmod(0o644)
return subprocess.CompletedProcess(args=args, returncode=0, stdout="{}", stderr="")
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
lark_cli._run_lark_cli_json(["/usr/bin/lark-cli", "auth", "login"], user_id="alice", timeout=5)
token_file = lark_cli.lark_cli_data_dir("alice") / "auth.json"
assert stat.S_IMODE(token_file.stat().st_mode) == 0o600
def test_lark_cli_env_from_runtime_uses_container_paths_for_sandbox_lark_commands():
runtime = SimpleNamespace(context={"user_id": "alice"})
env = _lark_cli_env_from_runtime(runtime, "/usr/bin/lark-cli auth status", sandbox_paths=True)
assert env is not None
assert env["LARKSUITE_CLI_CONFIG_DIR"] == lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR
assert env["LARKSUITE_CLI_DATA_DIR"] == lark_cli.LARK_CLI_SANDBOX_DATA_DIR
def test_lark_cli_env_from_runtime_ignores_non_lark_commands(tmp_path, monkeypatch):
_patch_paths(monkeypatch, tmp_path / "home")
runtime = SimpleNamespace(context={"user_id": "alice"})
assert _lark_cli_env_from_runtime(runtime, "echo hello", sandbox_paths=False) is None
def test_lark_auth_probe_distinguishes_local_configuration_from_live_verification(monkeypatch, tmp_path) -> None:
assert "verified" in lark_cli.LarkAuthProbe.__dataclass_fields__
_patch_paths(monkeypatch, tmp_path / "home")
config_file = lark_cli.lark_cli_config_dir("alice") / "config.json"
config_file.parent.mkdir(parents=True)
config_file.write_text(
json.dumps(
{
"currentApp": "cli_app",
"apps": [
{
"name": "cli_app",
"appId": "cli_app",
"appSecret": "secret",
"brand": "feishu",
}
],
}
),
encoding="utf-8",
)
calls: list[list[str]] = []
monkeypatch.setattr(lark_cli, "_resolve_lark_cli_path", lambda: "/usr/bin/lark-cli")
def _run(args, **_kwargs):
calls.append(args)
return subprocess.CompletedProcess(
args=args,
returncode=0,
stdout='{"identities":{"user":{"userName":"Alice"}}}',
stderr="",
)
monkeypatch.setattr(lark_cli.subprocess, "run", _run)
configured = lark_cli.probe_lark_auth("alice", verify=False)
live_verified = lark_cli.probe_lark_auth("alice", verify=True)
assert configured.status == "authenticated"
assert configured.verified is False
assert "not live-verified" in (configured.message or "")
assert live_verified.verified is True
assert "live-verified" in (live_verified.message or "")
assert calls[0] == ["/usr/bin/lark-cli", "auth", "status", "--json"]
assert calls[1] == ["/usr/bin/lark-cli", "auth", "status", "--json", "--verify"]
def test_complete_lark_auth_polls_device_code_and_returns_status(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
captured: dict[str, object] = {}
def _run_lark_cli_json(args, **kwargs):
captured["args"] = args
captured["kwargs"] = kwargs
return {}
monkeypatch.setattr(lark_cli, "_resolve_lark_cli_path", lambda: "/usr/bin/lark-cli")
monkeypatch.setattr(lark_cli, "_run_lark_cli_json", _run_lark_cli_json)
monkeypatch.setattr(
lark_cli,
"get_lark_integration_status",
lambda _user_id, _config, **_kwargs: lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="authenticated", user="Alice"),
),
)
result = lark_cli.complete_lark_auth("alice", config, device_code="device-code")
assert result.success is True
assert captured["args"] == [
"/usr/bin/lark-cli",
"auth",
"login",
"--device-code",
"device-code",
"--json",
]
assert captured["kwargs"] == {
"user_id": "alice",
"timeout": 45,
"allow_empty_success": True,
}
def test_complete_lark_auth_accepts_short_automatic_poll_timeout(monkeypatch, tmp_path) -> None:
assert "wait_timeout_seconds" in inspect.signature(lark_cli.complete_lark_auth).parameters
_patch_paths(monkeypatch, tmp_path / "home")
config = _config(tmp_path / "skills")
captured: dict[str, object] = {}
monkeypatch.setattr(lark_cli, "_require_lark_cli_path", lambda: "/usr/bin/lark-cli")
monkeypatch.setattr(
lark_cli,
"_run_lark_cli_json",
lambda _args, **kwargs: captured.update(kwargs) or {},
)
monkeypatch.setattr(
lark_cli,
"get_lark_integration_status",
lambda _user_id, _config, **_kwargs: lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="authenticated", user="Alice"),
),
)
result = lark_cli.complete_lark_auth(
"alice",
config,
device_code="device-code",
wait_timeout_seconds=8,
)
assert result.success is True
assert captured["timeout"] == 8
def test_auth_complete_request_bounds_poll_timeout() -> None:
model = integrations_router.LarkAuthCompleteRequest(device_code="device-code", wait_timeout_seconds=8)
assert "wait_timeout_seconds" in type(model).model_fields
assert model.wait_timeout_seconds == 8
with pytest.raises(ValueError):
integrations_router.LarkAuthCompleteRequest(device_code="device-code", wait_timeout_seconds=4)
with pytest.raises(ValueError):
integrations_router.LarkAuthCompleteRequest(device_code="device-code", wait_timeout_seconds=46)
def test_start_lark_config_returns_app_registration_url(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
monkeypatch.setattr(
lark_cli,
"_request_lark_app_registration_begin",
lambda _brand: {
"user_code": "abc",
"device_code": "config-device-code",
"expires_in": 600,
"interval": 5,
},
)
result = lark_cli.start_lark_config("alice", brand="feishu")
assert result.device_code == "config-device-code"
assert result.user_code == "abc"
assert result.verification_url.startswith("https://open.feishu.cn/page/cli?")
assert "user_code=abc" in result.verification_url
def test_complete_lark_config_saves_app_credentials_and_returns_status(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
captured: dict[str, object] = {}
monkeypatch.setattr(
lark_cli,
"_poll_lark_app_registration",
lambda **_kwargs: {
"client_id": "cli_mock",
"client_secret": "secret",
"user_info": {"tenant_brand": "feishu"},
},
)
monkeypatch.setattr(
lark_cli,
"_save_lark_app_config_with_cli",
lambda user_id, **kwargs: captured.update({"user_id": user_id, **kwargs}),
)
monkeypatch.setattr(
lark_cli,
"get_lark_integration_status",
lambda _user_id, _config, **_kwargs: lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="not_authorized", user=None),
),
)
result = lark_cli.complete_lark_config("alice", config, device_code="config-device-code", brand="feishu")
assert result.success is True
assert captured == {
"user_id": "alice",
"app_id": "cli_mock",
"app_secret": "secret",
"brand": "feishu",
}
def test_complete_lark_config_repolls_lark_tenant_for_client_secret(monkeypatch, tmp_path):
_patch_paths(monkeypatch, tmp_path / "home")
skills_root = tmp_path / "skills"
(skills_root / "public").mkdir(parents=True)
(skills_root / "custom").mkdir()
config = _config(skills_root)
poll_calls: list[dict[str, object]] = []
captured: dict[str, object] = {}
def _poll_lark_app_registration(**kwargs):
poll_calls.append(kwargs)
if kwargs["brand"] == "feishu":
return {
"client_id": "cli_mock",
"user_info": {"tenant_brand": "lark"},
}
return {
"client_id": "cli_mock",
"client_secret": "secret",
"user_info": {"tenant_brand": "lark"},
}
monkeypatch.setattr(lark_cli, "_poll_lark_app_registration", _poll_lark_app_registration)
monkeypatch.setattr(
lark_cli,
"_save_lark_app_config_with_cli",
lambda user_id, **kwargs: captured.update({"user_id": user_id, **kwargs}),
)
monkeypatch.setattr(
lark_cli,
"get_lark_integration_status",
lambda _user_id, _config, **_kwargs: lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="lark",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="not_authorized", user=None),
),
)
result = lark_cli.complete_lark_config("alice", config, device_code="config-device-code", brand="feishu")
assert result.success is True
assert [call["brand"] for call in poll_calls] == ["feishu", "lark"]
assert captured == {
"user_id": "alice",
"app_id": "cli_mock",
"app_secret": "secret",
"brand": "lark",
}
def _make_user(system_role: str) -> User:
return User(email=f"{system_role}-integration@example.com", password_hash="x", system_role=system_role, id=uuid4())
def _make_app(*, system_role: str, config):
app = make_authed_test_app(user_factory=lambda: _make_user(system_role))
app.dependency_overrides[get_config] = lambda: config
app.include_router(integrations_router.router)
return app
def test_lark_install_requires_admin(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
def _should_not_install(*args, **kwargs):
raise AssertionError("install should be admin-gated")
monkeypatch.setattr(integrations_router, "install_lark_integration", _should_not_install)
with TestClient(app) as client:
response = client.post("/api/integrations/lark/install")
assert response.status_code == 403
def test_lark_status_is_available_to_authenticated_users(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
monkeypatch.setattr(
integrations_router,
"get_lark_integration_status",
lambda _user_id, _config, **_kwargs: lark_cli.LarkIntegrationStatus(
installed=False,
version="v1.0.65",
manifest_version=None,
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=False,
app_id=None,
app_brand=None,
skills_expected=27,
skills_installed=0,
installed_skills=(),
enabled_skills=(),
install_path="/tmp/lark-cli",
cli=lark_cli.LarkCliProbe(available=False, error="missing"),
auth=lark_cli.LarkAuthProbe(status="unavailable", message="missing"),
),
)
with TestClient(app) as client:
response = client.get("/api/integrations/lark/status")
assert response.status_code == 200
assert response.json()["installed"] is False
def _status_with_host_paths() -> lark_cli.LarkIntegrationStatus:
return lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/home/deer-flow/.deer-flow/integrations/skills/lark-cli",
cli=lark_cli.LarkCliProbe(available=True, path="/usr/bin/lark-cli", version="1.0.65"),
auth=lark_cli.LarkAuthProbe(status="authenticated", user="alice"),
)
def test_lark_status_redacts_host_paths_for_non_admin(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
monkeypatch.setattr(integrations_router, "get_lark_integration_status", lambda *_a, **_k: _status_with_host_paths())
with TestClient(app) as client:
body = client.get("/api/integrations/lark/status").json()
assert body["install_path"] == ""
assert body["cli"]["path"] is None
# Non-sensitive fields are still reported.
assert body["installed"] is True
assert body["cli"]["version"] == "1.0.65"
def test_lark_status_exposes_host_paths_for_admin(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="admin", config=config)
monkeypatch.setattr(integrations_router, "get_lark_integration_status", lambda *_a, **_k: _status_with_host_paths())
with TestClient(app) as client:
body = client.get("/api/integrations/lark/status").json()
assert body["install_path"] == "/home/deer-flow/.deer-flow/integrations/skills/lark-cli"
assert body["cli"]["path"] == "/usr/bin/lark-cli"
def test_lark_config_start_route_returns_browser_url(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
monkeypatch.setattr(
integrations_router,
"start_lark_config",
lambda _user_id, **_kwargs: lark_cli.LarkConfigStartResult(
verification_url="https://open.feishu.cn/page/cli?user_code=config",
device_code="config-device-code",
expires_in=600,
interval=5,
user_code="config",
brand="feishu",
),
)
with TestClient(app) as client:
response = client.post("/api/integrations/lark/config/start", json={"brand": "feishu"})
assert response.status_code == 200
assert response.json()["verification_url"] == "https://open.feishu.cn/page/cli?user_code=config"
assert response.json()["device_code"] == "config-device-code"
def test_lark_config_complete_route_saves_app_credentials(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
monkeypatch.setattr(
integrations_router,
"complete_lark_config",
lambda _user_id, _config, *, device_code, **_kwargs: lark_cli.LarkConfigCompleteResult(
success=True,
message=f"configured {device_code}",
status=lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="not_authorized", user=None),
),
),
)
with TestClient(app) as client:
response = client.post(
"/api/integrations/lark/config/complete",
json={"device_code": "config-device-code", "brand": "feishu", "interval": 5, "expires_in": 600},
)
assert response.status_code == 200
assert response.json()["success"] is True
assert response.json()["status"]["app_configured"] is True
def test_lark_auth_start_route_returns_browser_url(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
captured_kwargs: dict[str, object] = {}
monkeypatch.setattr(
integrations_router,
"start_lark_auth",
lambda _user_id, **kwargs: (
captured_kwargs.update(kwargs)
or lark_cli.LarkAuthStartResult(
verification_url="https://open.feishu.cn/auth/mock",
device_code="device-code",
expires_in=600,
)
),
)
with TestClient(app) as client:
response = client.post("/api/integrations/lark/auth/start", json={})
assert response.status_code == 200
assert response.json()["verification_url"] == "https://open.feishu.cn/auth/mock"
assert response.json()["device_code"] == "device-code"
assert captured_kwargs == {"domains": (), "scope": None, "recommend": False}
def test_lark_auth_start_route_passes_explicit_recommend(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
captured_kwargs: dict[str, object] = {}
monkeypatch.setattr(
integrations_router,
"start_lark_auth",
lambda _user_id, **kwargs: (
captured_kwargs.update(kwargs)
or lark_cli.LarkAuthStartResult(
verification_url="https://open.feishu.cn/auth/mock",
device_code="device-code",
expires_in=600,
)
),
)
with TestClient(app) as client:
response = client.post("/api/integrations/lark/auth/start", json={"recommend": True})
assert response.status_code == 200
assert response.json()["verification_url"] == "https://open.feishu.cn/auth/mock"
assert response.json()["device_code"] == "device-code"
assert captured_kwargs == {"domains": (), "scope": None, "recommend": True}
def test_lark_auth_complete_route_polls_device_code(monkeypatch, tmp_path):
config = _config(tmp_path / "skills")
app = _make_app(system_role="user", config=config)
captured_kwargs = {}
def _complete_auth(_user_id, _config, **kwargs):
captured_kwargs.update(kwargs)
return lark_cli.LarkAuthCompleteResult(
success=True,
message=f"completed {kwargs['device_code']}",
status=lark_cli.LarkIntegrationStatus(
installed=True,
version="v1.0.65",
manifest_version="v1.0.65",
latest_available_version=None,
runtime_version_mismatch=False,
app_configured=True,
app_id="cli_mock",
app_brand="feishu",
skills_expected=27,
skills_installed=27,
installed_skills=("lark-doc",),
enabled_skills=("lark-doc",),
install_path="/tmp/lark",
cli=lark_cli.LarkCliProbe(available=True),
auth=lark_cli.LarkAuthProbe(status="authenticated", user="Alice", verified=True),
),
)
monkeypatch.setattr(integrations_router, "complete_lark_auth", _complete_auth)
with TestClient(app) as client:
response = client.post("/api/integrations/lark/auth/complete", json={"device_code": "device-code"})
assert response.status_code == 200
assert response.json()["success"] is True
assert response.json()["status"]["auth"]["status"] == "authenticated"
assert response.json()["status"]["auth"]["verified"] is True
assert captured_kwargs == {"device_code": "device-code", "wait_timeout_seconds": 45}