mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-05 04:18:47 +00:00
* 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.
458 lines
20 KiB
Python
458 lines
20 KiB
Python
"""Lark CLI sandbox credential broker (Pattern B, issue #4338).
|
|
|
|
Pattern A (PR #3971) provisions the ``lark-cli`` *binary* into the sandbox but
|
|
still mounts the per-user credential directories (``config`` with the long-lived
|
|
``appSecret`` and ``data`` with OAuth tokens) into the sandbox container, where
|
|
the agent's ``bash`` tool can read them.
|
|
|
|
This module implements the broker half of Pattern B: a long-lived process that
|
|
owns ``lark-cli`` + the credentials and exposes only the *command surface* over
|
|
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.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
from dataclasses import dataclass
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Loopback wire contract ────────────────────────────────────────────────
|
|
|
|
# The sandbox and the broker sidecar share the Pod network namespace, so the
|
|
# shim reaches the broker on loopback. The port is fixed and injected into the
|
|
# sandbox as DEERFLOW_LARK_BROKER_URL.
|
|
LARK_BROKER_DEFAULT_HOST = "127.0.0.1"
|
|
LARK_BROKER_DEFAULT_PORT = 8788
|
|
LARK_BROKER_URL_ENV = "DEERFLOW_LARK_BROKER_URL"
|
|
LARK_BROKER_EXEC_PATH = "/v1/exec"
|
|
LARK_BROKER_HEALTH_PATH = "/v1/health"
|
|
|
|
# Guards. Bounded so a compromised sandbox cannot exhaust the broker.
|
|
LARK_BROKER_MAX_REQUEST_BYTES = 1 * 1024 * 1024
|
|
LARK_BROKER_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
|
|
LARK_BROKER_DEFAULT_TIMEOUT_SECONDS = 120
|
|
LARK_BROKER_MAX_CONCURRENCY = 8
|
|
# Per-connection socket timeout. ThreadingHTTPServer spawns a thread per
|
|
# connection, so without this a sandbox could declare a large Content-Length and
|
|
# never send the body, parking a thread forever. Bounds the read so a slow/stuck
|
|
# client releases its thread. (Loopback-only, so this only guards the sandbox
|
|
# against tying up its own broker.)
|
|
LARK_BROKER_SOCKET_TIMEOUT_SECONDS = 30
|
|
|
|
# Optional env knob (comma-separated) for the subcommand denylist below.
|
|
LARK_BROKER_DENY_SUBCOMMANDS_ENV = "DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS"
|
|
|
|
# The runtime layout the sandbox sees is two files in ``bin/``:
|
|
#
|
|
# bin/lark-cli the POSIX-sh *launcher* (below) — the executable on PATH
|
|
# bin/lark-cli-shim.py the Python *shim* body (below) it execs
|
|
#
|
|
# Pattern A's launcher is pure ``#!/bin/sh`` (it just execs the arch-dispatched
|
|
# binary) and therefore has no runtime deps. The broker shim has to speak HTTP,
|
|
# so it is Python — but shipping it as a bare ``#!/usr/bin/env python3`` script
|
|
# would make every ``lark-cli`` call ENOEXEC/exit-127 on a sandbox image without
|
|
# ``python3`` on PATH, and because broker mode is opt-in that could slip past CI
|
|
# and only surface for an operator. The ``/bin/sh`` launcher resolves a Python 3
|
|
# interpreter itself (using only shell built-ins, so it still works when PATH is
|
|
# empty and the interpreter is pinned) and, if none exists, fails *loudly* with
|
|
# an actionable message instead of an opaque ENOEXEC. ``DEERFLOW_LARK_BROKER_PYTHON``
|
|
# pins a specific interpreter for images that ship Python under a non-standard
|
|
# name.
|
|
#
|
|
# The launcher's path to the shim body is baked in at install time rather than
|
|
# derived from ``$0``: when the sandbox runs ``lark-cli`` off PATH, ``$0`` is the
|
|
# bare command name with no directory, so a sibling lookup would fail. The
|
|
# install dir is a stable, shared mount, so the absolute path is valid in both
|
|
# the init container that writes it and the sandbox that reads it.
|
|
#
|
|
# Both scripts are kept here as the single source of truth and mirrored by the
|
|
# broker image build via ``install_shim``, exactly like ``LARK_CLI_SANDBOX_LAUNCHER_SCRIPT``
|
|
# for Pattern A, so the image copies can never drift from the Gateway's.
|
|
LARK_BROKER_PYTHON_ENV = "DEERFLOW_LARK_BROKER_PYTHON"
|
|
LARK_CLI_BROKER_SHIM_FILENAME = "lark-cli-shim.py"
|
|
_LARK_CLI_BROKER_SHIM_PATH_PLACEHOLDER = "@@LARK_CLI_BROKER_SHIM_PATH@@"
|
|
|
|
LARK_CLI_BROKER_LAUNCHER_TEMPLATE = (
|
|
"#!/bin/sh\n"
|
|
"# DeerFlow lark-cli broker launcher (Pattern B). Resolves a Python 3\n"
|
|
"# interpreter and execs the forwarding shim. Fails loudly (not with an opaque\n"
|
|
"# ENOEXEC) when the sandbox image ships no python3. Uses only shell built-ins\n"
|
|
"# so it still works when PATH is empty and DEERFLOW_LARK_BROKER_PYTHON pins\n"
|
|
"# the interpreter.\n"
|
|
"set -eu\n"
|
|
'shim="' + _LARK_CLI_BROKER_SHIM_PATH_PLACEHOLDER + '"\n'
|
|
'if [ -n "${DEERFLOW_LARK_BROKER_PYTHON:-}" ]; then\n'
|
|
' exec "$DEERFLOW_LARK_BROKER_PYTHON" "$shim" "$@"\n'
|
|
"fi\n"
|
|
"for _py in python3 python; do\n"
|
|
' if command -v "$_py" >/dev/null 2>&1; then\n'
|
|
' exec "$_py" "$shim" "$@"\n'
|
|
" fi\n"
|
|
"done\n"
|
|
'echo "lark-cli: broker mode needs a Python 3 interpreter but none was found;" >&2\n'
|
|
'echo " set DEERFLOW_LARK_BROKER_PYTHON to a python3 path in the sandbox image." >&2\n'
|
|
"exit 127\n"
|
|
)
|
|
|
|
|
|
def render_launcher_script(shim_path: str) -> str:
|
|
"""Render the ``/bin/sh`` launcher with the shim body's absolute path baked in."""
|
|
return LARK_CLI_BROKER_LAUNCHER_TEMPLATE.replace(_LARK_CLI_BROKER_SHIM_PATH_PLACEHOLDER, shim_path)
|
|
|
|
|
|
# The shim reads argv/stdin, POSTs to the broker, and replays the broker's
|
|
# stdout/stderr/exit code. On any transport failure it fails loudly and non-zero
|
|
# so a broker outage never looks like a successful lark-cli run. It is invoked as
|
|
# ``<python> lark-cli-shim.py <args...>`` by the launcher above (so it does not
|
|
# rely on its own shebang being resolvable), and stdin/argv pass straight through.
|
|
LARK_CLI_BROKER_SHIM_SCRIPT = r'''#!/usr/bin/env python3
|
|
"""DeerFlow lark-cli broker shim (Pattern B). Forwards argv/stdin to the broker.
|
|
|
|
Note: the broker runs lark-cli in the *sidecar's* working directory and cannot
|
|
see the sandbox filesystem, so cwd is intentionally not forwarded. Subcommands
|
|
that read/write files relative to the sandbox cwd are unsupported in broker mode.
|
|
"""
|
|
import base64
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
BROKER_URL = os.environ.get("DEERFLOW_LARK_BROKER_URL", "http://127.0.0.1:8788")
|
|
|
|
|
|
def _fail(message, code=127):
|
|
sys.stderr.write("lark-cli: " + message + "\n")
|
|
sys.exit(code)
|
|
|
|
|
|
def main():
|
|
try:
|
|
stdin_bytes = b"" if sys.stdin is None or sys.stdin.isatty() else sys.stdin.buffer.read()
|
|
except Exception:
|
|
stdin_bytes = b""
|
|
payload = json.dumps(
|
|
{
|
|
"args": sys.argv[1:],
|
|
"stdin_b64": base64.b64encode(stdin_bytes).decode("ascii"),
|
|
}
|
|
).encode("utf-8")
|
|
req = urllib.request.Request(
|
|
BROKER_URL.rstrip("/") + "/v1/exec",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=600) as resp:
|
|
body = json.loads(resp.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as exc:
|
|
detail = ""
|
|
try:
|
|
detail = json.loads(exc.read().decode("utf-8")).get("error", "")
|
|
except Exception:
|
|
detail = ""
|
|
_fail("broker rejected request (HTTP %d%s)" % (exc.code, ": " + detail if detail else ""))
|
|
except (urllib.error.URLError, OSError) as exc:
|
|
_fail("broker unreachable at %s (%s)" % (BROKER_URL, exc))
|
|
except Exception as exc: # noqa: BLE001
|
|
_fail("broker call failed (%s)" % exc)
|
|
sys.stdout.buffer.write(base64.b64decode(body.get("stdout_b64", "")))
|
|
sys.stdout.buffer.flush()
|
|
sys.stderr.buffer.write(base64.b64decode(body.get("stderr_b64", "")))
|
|
sys.stderr.buffer.flush()
|
|
sys.exit(int(body.get("exit_code", 1)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
'''
|
|
|
|
|
|
# ── Broker server ─────────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BrokerConfig:
|
|
"""Runtime configuration for the broker sidecar."""
|
|
|
|
lark_cli_path: str
|
|
config_dir: str
|
|
data_dir: str
|
|
host: str = LARK_BROKER_DEFAULT_HOST
|
|
port: int = LARK_BROKER_DEFAULT_PORT
|
|
timeout_seconds: int = LARK_BROKER_DEFAULT_TIMEOUT_SECONDS
|
|
# Opt-in denylist of ``lark-cli`` subcommand paths the broker refuses to run
|
|
# (issue #4338 hardening). Each entry is a space-joined command prefix, e.g.
|
|
# "config show" or "auth token", matched against the leading non-flag tokens
|
|
# of the request. Narrows the command surface a prompt-injected agent can
|
|
# reach — the broker already removes the credential *files*, but the full
|
|
# command surface stays reachable unless a secret-dumping subcommand is denied
|
|
# here. Empty by default (no behavior change).
|
|
deny_subcommands: tuple[tuple[str, ...], ...] = ()
|
|
|
|
def credential_env(self) -> dict[str, str]:
|
|
"""Env the broker injects into every lark-cli invocation.
|
|
|
|
The client never supplies these — the broker owns the credential paths,
|
|
so a sandbox process cannot point lark-cli at a different profile.
|
|
"""
|
|
return {
|
|
"LARKSUITE_CLI_CONFIG_DIR": self.config_dir,
|
|
"LARKSUITE_CLI_DATA_DIR": self.data_dir,
|
|
"LARKSUITE_CLI_NO_UPDATE_NOTIFIER": "1",
|
|
"LARKSUITE_CLI_NO_SKILLS_NOTIFIER": "1",
|
|
}
|
|
|
|
|
|
def parse_deny_subcommands(raw: str | None) -> tuple[tuple[str, ...], ...]:
|
|
"""Parse the comma-separated denylist env into command-prefix tuples.
|
|
|
|
``"config show, auth token"`` → ``(("config", "show"), ("auth", "token"))``.
|
|
Blank/whitespace-only entries are dropped.
|
|
"""
|
|
if not raw:
|
|
return ()
|
|
prefixes: list[tuple[str, ...]] = []
|
|
for entry in raw.split(","):
|
|
tokens = tuple(entry.split())
|
|
if tokens:
|
|
prefixes.append(tokens)
|
|
return tuple(prefixes)
|
|
|
|
|
|
def _denied_subcommand(deny: tuple[tuple[str, ...], ...], args: list[str]) -> tuple[str, ...] | None:
|
|
"""Return the matched denylist prefix if ``args`` is a denied subcommand.
|
|
|
|
Matches against the leading non-flag tokens (options and their values are
|
|
skipped) so ``config --json show`` is still caught by a ``config show`` rule.
|
|
"""
|
|
if not deny:
|
|
return None
|
|
positional = [token for token in args if not token.startswith("-")]
|
|
for prefix in deny:
|
|
if positional[: len(prefix)] == list(prefix):
|
|
return prefix
|
|
return None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExecResult:
|
|
exit_code: int
|
|
stdout: bytes
|
|
stderr: bytes
|
|
truncated: bool
|
|
|
|
|
|
def run_lark_cli(config: BrokerConfig, args: list[str], stdin: bytes) -> ExecResult:
|
|
"""Run a single ``lark-cli`` invocation with broker-owned credentials.
|
|
|
|
``args`` is passed as an argv list with ``shell=False`` so a sandbox-supplied
|
|
argument can never be shell-interpreted into a second command. A configured
|
|
``deny_subcommands`` prefix is refused before the binary is ever spawned.
|
|
"""
|
|
denied = _denied_subcommand(config.deny_subcommands, args)
|
|
if denied is not None:
|
|
message = f"lark-cli: subcommand '{' '.join(denied)}' is disabled in broker mode\n"
|
|
return ExecResult(126, b"", message.encode("utf-8"), False)
|
|
env = {**os.environ, **config.credential_env()}
|
|
try:
|
|
completed = subprocess.run( # noqa: S603 - argv list, shell=False, fixed binary
|
|
[config.lark_cli_path, *args],
|
|
input=stdin,
|
|
capture_output=True,
|
|
timeout=config.timeout_seconds,
|
|
check=False,
|
|
env=env,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
return ExecResult(124, b"", b"lark-cli: broker timed out\n", False)
|
|
except FileNotFoundError:
|
|
return ExecResult(127, b"", b"lark-cli: binary not found in broker\n", False)
|
|
|
|
stdout, out_trunc = _cap(completed.stdout or b"")
|
|
stderr, err_trunc = _cap(completed.stderr or b"")
|
|
return ExecResult(completed.returncode, stdout, stderr, out_trunc or err_trunc)
|
|
|
|
|
|
def _cap(data: bytes) -> tuple[bytes, bool]:
|
|
if len(data) <= LARK_BROKER_MAX_OUTPUT_BYTES:
|
|
return data, False
|
|
return data[:LARK_BROKER_MAX_OUTPUT_BYTES], True
|
|
|
|
|
|
def make_handler(config: BrokerConfig) -> type[BaseHTTPRequestHandler]:
|
|
"""Build a request handler bound to ``config``.
|
|
|
|
A bounded semaphore caps concurrency so a flood of sandbox calls cannot spawn
|
|
unbounded ``lark-cli`` subprocesses.
|
|
"""
|
|
semaphore = threading.BoundedSemaphore(LARK_BROKER_MAX_CONCURRENCY)
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
# Bound per-connection reads so a client that declares a large
|
|
# Content-Length and never sends the body cannot park its thread forever
|
|
# (ThreadingHTTPServer is one-thread-per-connection). stdlib reads this
|
|
# to arm socket timeouts.
|
|
timeout = LARK_BROKER_SOCKET_TIMEOUT_SECONDS
|
|
|
|
# Quiet: default BaseHTTPRequestHandler logs to stderr per request.
|
|
def log_message(self, *_args: Any) -> None: # noqa: D401
|
|
return
|
|
|
|
def _send_json(self, status: int, body: dict[str, Any]) -> None:
|
|
payload = json.dumps(body).encode("utf-8")
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
|
|
def do_GET(self) -> None: # noqa: N802 - stdlib API
|
|
if self.path.rstrip("/") == LARK_BROKER_HEALTH_PATH:
|
|
self._send_json(200, {"ok": True})
|
|
return
|
|
self._send_json(404, {"error": "not found"})
|
|
|
|
def do_POST(self) -> None: # noqa: N802 - stdlib API
|
|
if self.path.rstrip("/") != LARK_BROKER_EXEC_PATH:
|
|
self._send_json(404, {"error": "not found"})
|
|
return
|
|
try:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
except ValueError:
|
|
self._send_json(400, {"error": "bad content-length"})
|
|
return
|
|
if length <= 0 or length > LARK_BROKER_MAX_REQUEST_BYTES:
|
|
self._send_json(413, {"error": "request too large"})
|
|
return
|
|
try:
|
|
request = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
args = request["args"]
|
|
if not isinstance(args, list) or not all(isinstance(a, str) for a in args):
|
|
raise ValueError("args must be a list of strings")
|
|
stdin = base64.b64decode(request.get("stdin_b64", "") or "")
|
|
except Exception: # noqa: BLE001 - untrusted client input
|
|
self._send_json(400, {"error": "invalid request"})
|
|
return
|
|
|
|
if not semaphore.acquire(blocking=False):
|
|
self._send_json(503, {"error": "broker busy"})
|
|
return
|
|
try:
|
|
result = run_lark_cli(config, args, stdin)
|
|
except Exception: # noqa: BLE001 - keep the wire contract uniform
|
|
# run_lark_cli already maps the expected failures (timeout,
|
|
# missing binary) to ExecResults; anything else (OSError,
|
|
# PermissionError, …) would otherwise close the connection with
|
|
# no body and surface as an opaque transport error in the shim.
|
|
# Return a structured 500 so the shim reports a meaningful error.
|
|
logger.exception("lark-cli exec failed unexpectedly")
|
|
self._send_json(500, {"error": "broker exec failed"})
|
|
return
|
|
finally:
|
|
semaphore.release()
|
|
|
|
self._send_json(
|
|
200,
|
|
{
|
|
"exit_code": result.exit_code,
|
|
"stdout_b64": base64.b64encode(result.stdout).decode("ascii"),
|
|
"stderr_b64": base64.b64encode(result.stderr).decode("ascii"),
|
|
"truncated": result.truncated,
|
|
},
|
|
)
|
|
|
|
return Handler
|
|
|
|
|
|
def serve(config: BrokerConfig) -> ThreadingHTTPServer:
|
|
"""Start the broker HTTP server bound to loopback and return it."""
|
|
if not shutil.which(config.lark_cli_path) and not os.path.isfile(config.lark_cli_path):
|
|
logger.warning("lark-cli not found at %s; broker will report 127 for exec", config.lark_cli_path)
|
|
server = ThreadingHTTPServer((config.host, config.port), make_handler(config))
|
|
logger.info("lark-cli broker listening on %s:%d", config.host, config.port)
|
|
return server
|
|
|
|
|
|
def install_shim(dest_dir: str, *, version: str | None = None) -> str:
|
|
"""Write the launcher + shim + runtime marker into the sandbox runtime dir.
|
|
|
|
Called by the broker image's ``install-shim`` init-container mode. Produces
|
|
the same ``bin/lark-cli`` + ``.deerflow-lark-cli-runtime.json`` layout Pattern
|
|
A stages, but marked ``kind="shim"`` so the runtime validator knows the
|
|
``linux-*`` binaries are intentionally absent (the sidecar holds the real
|
|
binary).
|
|
|
|
Two files are written into ``bin/``: the executable-on-PATH ``lark-cli`` is a
|
|
``/bin/sh`` *launcher* that resolves a Python 3 interpreter and execs the
|
|
``lark-cli-shim.py`` *body* next to it. Splitting them keeps broker mode from
|
|
silently failing with ENOEXEC on a sandbox image that has no ``python3`` (the
|
|
launcher fails loudly with an actionable message instead). Both come from the
|
|
in-process constants so the image copy can never drift from the Gateway's.
|
|
"""
|
|
dest = os.path.abspath(dest_dir)
|
|
bin_dir = os.path.join(dest, "bin")
|
|
os.makedirs(bin_dir, exist_ok=True)
|
|
shim_body = os.path.join(bin_dir, LARK_CLI_BROKER_SHIM_FILENAME)
|
|
with open(shim_body, "w", encoding="utf-8") as handle:
|
|
handle.write(LARK_CLI_BROKER_SHIM_SCRIPT)
|
|
os.chmod(shim_body, 0o755)
|
|
launcher = os.path.join(bin_dir, "lark-cli")
|
|
with open(launcher, "w", encoding="utf-8") as handle:
|
|
handle.write(render_launcher_script(shim_body))
|
|
os.chmod(launcher, 0o755)
|
|
marker = os.path.join(dest, ".deerflow-lark-cli-runtime.json")
|
|
with open(marker, "w", encoding="utf-8") as handle:
|
|
json.dump({"version": version or "unknown", "kind": "shim"}, handle)
|
|
return launcher
|
|
|
|
|
|
def _config_from_env() -> BrokerConfig:
|
|
return BrokerConfig(
|
|
lark_cli_path=os.environ.get("DEERFLOW_LARK_BROKER_CLI", "lark-cli"),
|
|
config_dir=os.environ.get("LARKSUITE_CLI_CONFIG_DIR", "/var/lark/config"),
|
|
data_dir=os.environ.get("LARKSUITE_CLI_DATA_DIR", "/var/lark/data"),
|
|
host=os.environ.get("DEERFLOW_LARK_BROKER_HOST", LARK_BROKER_DEFAULT_HOST),
|
|
port=int(os.environ.get("DEERFLOW_LARK_BROKER_PORT", str(LARK_BROKER_DEFAULT_PORT))),
|
|
timeout_seconds=int(os.environ.get("DEERFLOW_LARK_BROKER_TIMEOUT", str(LARK_BROKER_DEFAULT_TIMEOUT_SECONDS))),
|
|
deny_subcommands=parse_deny_subcommands(os.environ.get(LARK_BROKER_DENY_SUBCOMMANDS_ENV)),
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
logging.basicConfig(level=logging.INFO)
|
|
argv = sys.argv[1:]
|
|
if argv and argv[0] == "install-shim":
|
|
dest = argv[1] if len(argv) > 1 else os.environ.get("LARK_CLI_RUNTIME_DEST", "/mnt/integrations/lark-cli/runtime")
|
|
launcher = install_shim(dest, version=os.environ.get("LARK_CLI_VERSION"))
|
|
logger.info("Installed lark-cli broker shim at %s", launcher)
|
|
return
|
|
server = serve(_config_from_env())
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
server.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|