mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 00:17:53 +00:00
* feat: add lark cli integration * fix: polish lark integration actions * feat: support lark incremental permissions * fix: detect lark authorization completion * fix: harden lark integration install * feat: expand lark auth scopes and reuse host auth in sandbox Default lark auth to least-privilege (recommend=false, base sign-in only) and expose the full set of lark-cli --domain business domains as native --domain grants instead of a 4-domain read-only mapping. Resolve the skill pack from the latest larksuite/cli GitHub release at install time with content-hash integrity, and surface version/runtime drift in status. Share the per-user lark-cli config/data profile between the Gateway Settings auth flow and agent conversations by mounting the integration dirs into the AIO sandbox and injecting the matching env for lark-cli commands, with an allowlisted extra_mounts path in the provisioner/K8s backend and traversal guards on integration paths. * style: fix lint issues from ruff and prettier Sort imports in the provisioner PVC test and re-wrap two long i18n description strings to satisfy backend ruff and frontend prettier CI. * fix(lark): address managed integration review feedback * fix(frontend): stabilize integrations settings e2e * test(sandbox): isolate remote backend legacy visibility check * test: fix backend unit failures after merge * Harden Lark integration review fixes * Format Lark integration E2E test * fix(lark): harden sandbox credential exposure and status disclosure Address willem_bd's security review on PR #3971: - Mount the per-user lark-cli config dir (long-lived appSecret) read-only into the AIO sandbox; only the refreshable-token data dir stays writable. - Redact host filesystem paths (install_path, cli.path) from GET /lark/status and the config/auth complete responses for non-admin callers, fail-closed on any auth error. - Document the npm postinstall trade-off (--ignore-scripts is not viable because @larksuite/cli fetches its platform binary in postinstall). - Document the sandbox credential trust boundary in AGENTS.md and README, pointing at the sidecar-broker follow-up (#4338). --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Regression anchors: integrations router must not block the event loop.
|
|
|
|
The Lark integration handlers are async FastAPI route handlers, but the work
|
|
they dispatch includes zip reads, filesystem staging, manifest writes, and
|
|
``lark-cli`` subprocess calls. Those phases must stay behind
|
|
``asyncio.to_thread``; if a future refactor runs them inline, the strict
|
|
Blockbuster gate raises ``BlockingError`` and these anchors fail.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
import zipfile
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from app.gateway.routers import integrations
|
|
from deerflow.config import paths as paths_module
|
|
from deerflow.integrations import lark_cli
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
def _skill_content(name: str) -> str:
|
|
return f"---\nname: {name}\ndescription: {name} integration skill\n---\n\n# {name}\n"
|
|
|
|
|
|
def _build_lark_archive(archive: Path) -> None:
|
|
archive.parent.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
for skill_name in lark_cli.LARK_SKILL_NAMES:
|
|
zf.writestr(f"cli-1.0.65/skills/{skill_name}/SKILL.md", _skill_content(skill_name))
|
|
zf.writestr(f"cli-1.0.65/skills/{skill_name}/references/readme.md", f"# {skill_name}\n")
|
|
|
|
|
|
def _write_stub_lark_cli(path: Path) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(
|
|
"""#!/bin/sh
|
|
if [ "$1" = "--version" ]; then
|
|
echo "v1.0.65"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$1" = "auth" ] && [ "$2" = "login" ]; then
|
|
echo "{}"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$1" = "auth" ] && [ "$2" = "status" ]; then
|
|
echo '{"identities":{"user":{"userName":"Alice"}}}'
|
|
exit 0
|
|
fi
|
|
|
|
echo "{}"
|
|
exit 0
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
path.chmod(0o755)
|
|
|
|
|
|
def _reset_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path / "home"))
|
|
monkeypatch.setattr(paths_module, "_paths", None)
|
|
|
|
|
|
async def _config(tmp_path: Path) -> SimpleNamespace:
|
|
skills_root = tmp_path / "skills"
|
|
await asyncio.to_thread((skills_root / "public").mkdir, parents=True, exist_ok=True)
|
|
await asyncio.to_thread((skills_root / "custom").mkdir, parents=True, exist_ok=True)
|
|
return SimpleNamespace(
|
|
skills=SimpleNamespace(
|
|
get_skills_path=lambda: skills_root,
|
|
container_path="/mnt/skills",
|
|
use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage",
|
|
)
|
|
)
|
|
|
|
|
|
async def test_lark_install_route_does_not_block_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_reset_paths(tmp_path, monkeypatch)
|
|
config = await _config(tmp_path)
|
|
archive = tmp_path / "fixtures" / "lark-cli.zip"
|
|
await asyncio.to_thread(_build_lark_archive, archive)
|
|
|
|
async def _allow_admin(*_args, **_kwargs) -> None:
|
|
return None
|
|
|
|
refresh_calls = 0
|
|
|
|
async def _refresh_cache() -> None:
|
|
nonlocal refresh_calls
|
|
refresh_calls += 1
|
|
|
|
monkeypatch.setenv(lark_cli.LARK_CLI_SOURCE_ARCHIVE_ENV, str(archive))
|
|
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(integrations, "get_effective_user_id", lambda: "loop-user")
|
|
monkeypatch.setattr(integrations, "require_admin_user", _allow_admin)
|
|
monkeypatch.setattr(integrations, "refresh_skills_system_prompt_cache_async", _refresh_cache, raising=False)
|
|
|
|
response = await integrations.install_lark(request=None, config=config)
|
|
|
|
assert response.success is True
|
|
assert refresh_calls == 1
|
|
install_root = await asyncio.to_thread(lark_cli.lark_integration_root, "loop-user")
|
|
assert await asyncio.to_thread((install_root / "lark-doc" / "SKILL.md").exists)
|
|
|
|
|
|
async def test_lark_auth_complete_route_does_not_block_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_reset_paths(tmp_path, monkeypatch)
|
|
config = await _config(tmp_path)
|
|
cli_path = tmp_path / "bin" / "lark-cli"
|
|
await asyncio.to_thread(_write_stub_lark_cli, cli_path)
|
|
|
|
monkeypatch.setenv("PATH", f"{cli_path.parent}{os.pathsep}{os.environ.get('PATH', '')}")
|
|
monkeypatch.setattr(integrations, "get_effective_user_id", lambda: "loop-user")
|
|
|
|
response = await integrations.complete_lark_browser_auth(
|
|
request=None,
|
|
body=integrations.LarkAuthCompleteRequest(device_code="device-code"),
|
|
config=config,
|
|
)
|
|
|
|
assert response.status.cli.available is True
|
|
assert response.status.cli.version == "v1.0.65"
|