mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(docker): bundle Lark broker runtime helpers (#7542)
* fix(docker): bundle Lark broker runtime helpers * docs(lark): document standalone broker import dependencies
This commit is contained in:
parent
aabfdeebc9
commit
c5193d26c7
@ -1920,6 +1920,10 @@ overwriting them with an unevaluated fallback — so the Settings UI shows
|
||||
whether `lark-cli` will actually be present in the sandbox at chat time, rather
|
||||
than a green status hiding a later `command not found`.
|
||||
|
||||
The optional [Lark CLI broker image](docker/lark-cli-broker/README.md) bundles
|
||||
its required Python helper modules, so the sidecar runs without a separate
|
||||
DeerFlow harness installation.
|
||||
|
||||
In Lark broker mode, AIO's fresh Bash runs close inherited pipe stdin. Persistent
|
||||
terminal commands keep their input; the shim ignores terminal stdin. Explicit
|
||||
pipelines, heredocs, and file redirections still supply input normally, and the
|
||||
|
||||
@ -11,8 +11,8 @@ loopback. The sandbox gets a tiny ``lark-cli`` shim on ``PATH`` that forwards
|
||||
argv/stdin to the broker, so the raw credential files never exist in the sandbox
|
||||
filesystem.
|
||||
|
||||
Everything here is Python-3-stdlib only so the same module can run inside the
|
||||
minimal broker sidecar image without extra dependencies.
|
||||
The broker and its bundled DeerFlow utility modules use only Python's standard
|
||||
library, so the minimal sidecar needs no third-party Python packages.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -32,6 +32,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Keep the standalone image's Python COPY list in sync with this import and its
|
||||
# transitive dependencies: docker/lark-cli-broker/Dockerfile.
|
||||
from deerflow.utils.atomic_write import cleanup_failed_atomic_write
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# Backend Tests
|
||||
|
||||
`test_lark_broker_image.py` stages Python files from the broker Dockerfile's
|
||||
final-stage COPY instructions. Run shim installation and the health endpoint
|
||||
under `-I -S`, adding only the staged script directory to the import path;
|
||||
repository paths or installed Harness packages would hide missing image files.
|
||||
|
||||
When adding a scoped `AGENTS.md`, update `EXPECTED_GUIDANCE_PATHS` in
|
||||
`test_agent_guidance_check.py` in the same change. The explicit inventory pins
|
||||
guidance ownership and extends hard-budget checks to each approved guide.
|
||||
|
||||
95
backend/tests/test_lark_broker_image.py
Normal file
95
backend/tests/test_lark_broker_image.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""Exercise the broker image's Python file layout without an installed harness."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_broker(tmp_path: Path) -> Path:
|
||||
dockerfile = REPO_ROOT / "docker/lark-cli-broker/Dockerfile"
|
||||
runtime_stage = dockerfile.read_text(encoding="utf-8").replace("\\\n", " ").rsplit("\nFROM ", 1)[1]
|
||||
image_root = tmp_path / "image"
|
||||
for instruction in runtime_stage.splitlines():
|
||||
words = shlex.split(instruction, comments=True)
|
||||
if not words or words[0] != "COPY" or words[1].startswith("--from="):
|
||||
continue
|
||||
for source in words[1:-1]:
|
||||
if not source.endswith(".py"):
|
||||
continue
|
||||
destination = image_root / words[-1].lstrip("/")
|
||||
if words[-1].endswith("/"):
|
||||
destination /= Path(source).name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(REPO_ROOT / source, destination)
|
||||
return image_root / "opt/broker/lark_broker.py"
|
||||
|
||||
|
||||
def run_isolated(broker: Path, code: str, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
bootstrap = "import runpy, sys\nfrom pathlib import Path\nbroker = Path(sys.argv.pop(1))\nsys.path.insert(0, str(broker.parent))\n"
|
||||
# -I ignores inherited PYTHONPATH and -S skips installed editable packages.
|
||||
# Only the image's script directory is added back, matching its entrypoint.
|
||||
return subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-c", bootstrap + textwrap.dedent(code), str(broker), *args],
|
||||
cwd=broker.parent,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def test_image_installs_shim_without_harness(image_broker: Path, tmp_path: Path) -> None:
|
||||
runtime = tmp_path / "runtime"
|
||||
completed = run_isolated(image_broker, 'runpy.run_path(str(broker), run_name="__main__")', "install-shim", str(runtime))
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
launcher = runtime / "bin/lark-cli"
|
||||
shim = runtime / "bin/lark-cli-shim.py"
|
||||
assert launcher.read_text(encoding="utf-8").startswith("#!/bin/sh\n")
|
||||
assert str(shim) in launcher.read_text(encoding="utf-8")
|
||||
assert shim.read_text(encoding="utf-8").startswith("#!/usr/bin/env python3\n")
|
||||
assert json.loads((runtime / ".deerflow-lark-cli-runtime.json").read_text(encoding="utf-8"))["kind"] == "shim"
|
||||
|
||||
|
||||
def test_image_serves_health_without_harness(image_broker: Path) -> None:
|
||||
completed = run_isolated(
|
||||
image_broker,
|
||||
"""
|
||||
import json
|
||||
import threading
|
||||
from http.client import HTTPConnection
|
||||
|
||||
module = runpy.run_path(str(broker))
|
||||
config = module["BrokerConfig"](lark_cli_path=sys.executable, config_dir="config", data_dir="data", port=0)
|
||||
with module["serve"](config) as server:
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
connection = HTTPConnection(*server.server_address[:2], timeout=5)
|
||||
try:
|
||||
connection.request("GET", "/v1/health")
|
||||
response = connection.getresponse()
|
||||
assert response.status == 200
|
||||
assert json.loads(response.read()) == {"ok": True}
|
||||
finally:
|
||||
connection.close()
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
assert not thread.is_alive()
|
||||
""",
|
||||
)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
@ -54,15 +54,18 @@ COPY docker/lark-cli-init/build-runtime.sh /usr/local/bin/build-runtime.sh
|
||||
RUN chmod +x /usr/local/bin/build-runtime.sh \
|
||||
&& LARK_CLI_VERSION="${LARK_CLI_VERSION}" /usr/local/bin/build-runtime.sh /opt/lark-cli
|
||||
|
||||
# ── Final image: python3 + the staged real binary + the broker module ────────
|
||||
# ── Final image: python3 + the staged real binary + the broker runtime ───────
|
||||
FROM python:3.12-slim
|
||||
|
||||
ARG LARK_CLI_VERSION=v1.0.65
|
||||
ENV LARK_CLI_VERSION=${LARK_CLI_VERSION}
|
||||
|
||||
COPY --from=builder /opt/lark-cli /opt/lark-cli
|
||||
# Single stdlib-only module drives both install-shim and serve.
|
||||
# Bundle the broker's stdlib-only dependency closure without installing the harness.
|
||||
COPY backend/packages/harness/deerflow/integrations/lark_broker.py /opt/broker/lark_broker.py
|
||||
COPY backend/packages/harness/deerflow/utils/atomic_write.py \
|
||||
backend/packages/harness/deerflow/utils/file_revision_metadata.py \
|
||||
/opt/broker/deerflow/utils/
|
||||
COPY docker/lark-cli-broker/entrypoint.sh /usr/local/bin/lark-cli-broker
|
||||
RUN chmod +x /usr/local/bin/lark-cli-broker
|
||||
|
||||
|
||||
@ -39,6 +39,9 @@ image ships Python 3, so the default path needs no configuration.
|
||||
|
||||
## Build
|
||||
|
||||
The image bundles the broker script and its shared atomic-write helper modules.
|
||||
It requires no separate DeerFlow harness installation for either mode.
|
||||
|
||||
Build context is the **repo root** (the broker module lives under `backend/`):
|
||||
|
||||
```bash
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
#!/bin/sh
|
||||
# DeerFlow lark-cli broker entrypoint (Pattern B, issue #4338).
|
||||
#
|
||||
# Dispatches to one of two modes on the single stdlib-only module:
|
||||
# Dispatches to one of two modes on the broker script:
|
||||
#
|
||||
# install-shim [dest] write the shim + runtime marker into the shared
|
||||
# emptyDir (init-container mode), then exit 0.
|
||||
# serve run the broker HTTP server on loopback (sidecar mode).
|
||||
#
|
||||
# The module is copied to /opt/broker/lark_broker.py and run as a script; it has
|
||||
# no third-party dependencies, so no harness install is needed in this image.
|
||||
# The script and its shared helpers live under /opt/broker and use only the
|
||||
# Python standard library; no harness installation is needed in this image.
|
||||
set -eu
|
||||
|
||||
MODULE="/opt/broker/lark_broker.py"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user