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:
zenghao 2026-10-27 00:19:23 +08:00 committed by GitHub
parent aabfdeebc9
commit c5193d26c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 119 additions and 7 deletions

View File

@ -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

View File

@ -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__)

View File

@ -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.

View 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

View File

@ -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

View File

@ -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

View File

@ -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"