fix(lark): keep CLI lock directory writable in sandboxes (#4701)

* fix(lark): provide writable CLI lock directory

* test(lark): pin nested lock mount ordering
This commit is contained in:
Creeper998 2026-08-10 11:01:14 +08:00 committed by GitHub
parent e401ae2d7b
commit 17531d7c11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 94 additions and 36 deletions

View File

@ -791,8 +791,10 @@ set `DEER_FLOW_LARK_CLI_SANDBOX_RUNTIME_DIR` to that directory.
> **Sandbox trust boundary:** the browser never receives the Lark app secret, but > **Sandbox trust boundary:** the browser never receives the Lark app secret, but
> agent conversations run `lark-cli` inside the sandbox, so the per-user > agent conversations run `lark-cli` inside the sandbox, so the per-user
> credential directories are mounted into it: `config` (holding the long-lived > credential directories are mounted into it: `config` (holding the long-lived
> `appSecret`) is mounted **read-only** and `data` (refreshable OAuth tokens) > `appSecret`) is mounted **read-only**, its otherwise empty `config/locks`
> writable. Both remain *readable* by any process the agent runs there, so code > subdirectory is over-mounted writable for `lark-cli` coordination files, and
> `data` (refreshable OAuth tokens) is writable. The credential-bearing config
> and data mounts remain *readable* by any process the agent runs there, so code
> reached via prompt injection in a tool result could read them. Treat the > reached via prompt injection in a tool result could read them. Treat the
> sandbox as inside the Lark credential trust boundary until the sidecar > sandbox as inside the Lark credential trust boundary until the sidecar
> credential-broker follow-up removes these mounts from sandbox execution. > credential-broker follow-up removes these mounts from sandbox execution.

View File

@ -877,7 +877,7 @@ E2B output sync records remote file versions and actual host file metadata in a
- `skills/describe.py``build_describe_skill_tool(catalog)` builds the `describe_skill` tool as a closure; `build_skill_search_setup(skills, enabled, ...)` produces a `SkillSearchSetup(describe_skill_tool, skill_names)` that is wired into both the LangGraph agent factory (`agent.py`) and the embedded client (`client.py`). - `skills/describe.py``build_describe_skill_tool(catalog)` builds the `describe_skill` tool as a closure; `build_skill_search_setup(skills, enabled, ...)` produces a `SkillSearchSetup(describe_skill_tool, skill_names)` that is wired into both the LangGraph agent factory (`agent.py`) and the embedded client (`client.py`).
- **Slash activation**: `/skill-name task` loads that enabled skill's `SKILL.md` for the current model call only. The resolver rejects leading whitespace, missing separators, reserved channel commands (`/new`, `/help`, `/bootstrap`, `/status`, `/models`, `/memory`, `/goal`), disabled skills, and skills outside a custom agent's whitelist. - **Slash activation**: `/skill-name task` loads that enabled skill's `SKILL.md` for the current model call only. The resolver rejects leading whitespace, missing separators, reserved channel commands (`/new`, `/help`, `/bootstrap`, `/status`, `/models`, `/memory`, `/goal`), disabled skills, and skills outside a custom agent's whitelist.
- **Installation**: `POST /api/skills/install` extracts .skill ZIP archive to custom/ directory - **Installation**: `POST /api/skills/install` extracts .skill ZIP archive to custom/ directory
- **Managed integrations**: Lark/Feishu CLI support installs one global official `lark-*` pack as read-only `SkillCategory.INTEGRATION` entries under `/mnt/skills/integrations/lark-cli/...`; enabled flags, app configuration, and OAuth data remain per-user. Install resolves the newest `larksuite/cli` release from GitHub (`releases/latest`) at install time (falling back to a bottom-line pinned version if the lookup fails) rather than hard-coding the pack version; integrity relies on the official host + structural archive guards + a recorded hash of the effective installed tree after shared guidance injection (not a pinned archive-byte SHA, which GitHub does not keep stable). The Gateway image still installs a pinned `@larksuite/cli` binary, so `get_lark_integration_status` surfaces `latest_available_version` and `runtime_version_mismatch` for the UI. AIO installs additionally verify and publish official Linux amd64/arm64 binaries under `{DEER_FLOW_HOME}/integrations/lark-cli/sandbox-cli`, mounted read-only at `/mnt/integrations/lark-cli/runtime`; `/mnt/integrations/lark-cli/config` (app credentials, incl. the long-lived `appSecret`) is mounted **read-only** into the sandbox and `/mnt/integrations/lark-cli/data` (refreshable OAuth tokens) stays writable, both mapping to owner-only per-user credential directories. **Sandbox trust boundary:** those two dirs are still *readable* by arbitrary sandbox processes (the agent's `bash` tool, or code reached via prompt-injection in a tool result), so the app secret and tokens are exposed to sandbox-side code even though they never reach the browser — the read-only config mount only prevents in-sandbox tampering, not read/exfiltration. The sidecar credential-broker (Pattern B, issue #4338) is the fix that removes these plaintext mounts from sandbox execution: set `LARK_CLI_BROKER_IMAGE` on the provisioner (see `docker/lark-cli-broker/`) and the Gateway sends `provision_lark_cli_broker` on sandbox create. The provisioner then runs a `lark-cli-broker` sidecar that owns the per-user `config`/`data` (mounted into the **sidecar only**, at `/var/lark/{config,data}`) and serves the `lark-cli` command surface on Pod loopback (`http://127.0.0.1:8788`); a shim init container (`install-shim`) writes a forwarding `lark-cli` into the shared runtime `emptyDir`, so the sandbox gets `DEERFLOW_LARK_BROKER_URL` + a shim on PATH but **no** credential files. The on-PATH `bin/lark-cli` is a `/bin/sh` launcher that resolves a Python 3 interpreter and execs the Python shim body (`bin/lark-cli-shim.py`) beside it, so broker mode does not silently ENOEXEC on a sandbox image without a `#!/usr/bin/env python3`-resolvable interpreter — it fails loudly (exit 127, actionable message) and can be pinned with `DEERFLOW_LARK_BROKER_PYTHON`. The broker runs `lark-cli` in the sidecar's cwd and cannot see the sandbox filesystem, so cwd is intentionally **not** forwarded and file-I/O subcommands relative to the sandbox cwd are unsupported (command surface only). An optional `DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS` denylist (comma-separated command prefixes, forwarded from the provisioner) lets the broker refuse secret-dumping subcommands before spawning the binary. `lark_cli_env_overlay(broker=True)` therefore omits `LARKSUITE_CLI_CONFIG_DIR`/`DATA_DIR`; `sandbox_lark_broker_active()` (TTL-cached provisioner `/api/capabilities` probe, tight timeout + longer negative caching on the bash hot path) selects broker vs. binary mode for both the bash env overlay and status. `DEER_FLOW_LARK_CLI_SANDBOX_RUNTIME_DIR` supplies a validated, symlink-free pre-staged runtime for air-gapped deployments. For the remote provisioner (K8s), the runtime binary is otherwise provisioned by an optional init container + shared `emptyDir` (Pattern A): set `LARK_CLI_INIT_IMAGE` on the provisioner (see `docker/lark-cli-init/`) and the Gateway sends `provision_lark_cli_runtime` on sandbox create once the pack is installed, so remote installs skip the Gateway-side GitHub download entirely. Broker (Pattern B) supersedes the init-container binary (Pattern A) when both images are configured. `get_lark_integration_status(check_runtime=True)` surfaces `sandbox_runtime_mode` (`none` / `gateway-download` / `init-container` / `broker`) and `sandbox_runtime_ready` (remote modes read the provisioner `GET /api/capabilities`: `lark_cli_init_image` / `lark_cli_broker_image`) so a green UI can't hide a chat-time `lark-cli: command not found`. Cheap status probes are explicitly not live-verified; users authorize or reconnect through the browser device-flow endpoints instead of running terminal commands. - **Managed integrations**: Lark/Feishu CLI support installs one global official `lark-*` pack as read-only `SkillCategory.INTEGRATION` entries under `/mnt/skills/integrations/lark-cli/...`; enabled flags, app configuration, and OAuth data remain per-user. Install resolves the newest `larksuite/cli` release from GitHub (`releases/latest`) at install time (falling back to a bottom-line pinned version if the lookup fails) rather than hard-coding the pack version; integrity relies on the official host + structural archive guards + a recorded hash of the effective installed tree after shared guidance injection (not a pinned archive-byte SHA, which GitHub does not keep stable). The Gateway image still installs a pinned `@larksuite/cli` binary, so `get_lark_integration_status` surfaces `latest_available_version` and `runtime_version_mismatch` for the UI. AIO installs additionally verify and publish official Linux amd64/arm64 binaries under `{DEER_FLOW_HOME}/integrations/lark-cli/sandbox-cli`, mounted read-only at `/mnt/integrations/lark-cli/runtime`; `/mnt/integrations/lark-cli/config` (app credentials, incl. the long-lived `appSecret`) is mounted **read-only** into the sandbox, its empty `config/locks` subdirectory is over-mounted writable for `lark-cli` coordination files, and `/mnt/integrations/lark-cli/data` (refreshable OAuth tokens) stays writable, all mapping to owner-only per-user directories. **Sandbox trust boundary:** the credential-bearing config and data dirs are still *readable* by arbitrary sandbox processes (the agent's `bash` tool, or code reached via prompt-injection in a tool result), so the app secret and tokens are exposed to sandbox-side code even though they never reach the browser — the read-only config mount only prevents in-sandbox tampering, not read/exfiltration. The sidecar credential-broker (Pattern B, issue #4338) is the fix that removes these plaintext mounts from sandbox execution: set `LARK_CLI_BROKER_IMAGE` on the provisioner (see `docker/lark-cli-broker/`) and the Gateway sends `provision_lark_cli_broker` on sandbox create. The provisioner then runs a `lark-cli-broker` sidecar that owns the per-user `config`/`config/locks`/`data` mounts (mounted into the **sidecar only**, at `/var/lark/{config,config/locks,data}` with only the nested locks mount writable) and serves the `lark-cli` command surface on Pod loopback (`http://127.0.0.1:8788`); a shim init container (`install-shim`) writes a forwarding `lark-cli` into the shared runtime `emptyDir`, so the sandbox gets `DEERFLOW_LARK_BROKER_URL` + a shim on PATH but **no** credential files. The on-PATH `bin/lark-cli` is a `/bin/sh` launcher that resolves a Python 3 interpreter and execs the Python shim body (`bin/lark-cli-shim.py`) beside it, so broker mode does not silently ENOEXEC on a sandbox image without a `#!/usr/bin/env python3`-resolvable interpreter — it fails loudly (exit 127, actionable message) and can be pinned with `DEERFLOW_LARK_BROKER_PYTHON`. The broker runs `lark-cli` in the sidecar's cwd and cannot see the sandbox filesystem, so cwd is intentionally **not** forwarded and file-I/O subcommands relative to the sandbox cwd are unsupported (command surface only). An optional `DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS` denylist (comma-separated command prefixes, forwarded from the provisioner) lets the broker refuse secret-dumping subcommands before spawning the binary. `lark_cli_env_overlay(broker=True)` therefore omits `LARKSUITE_CLI_CONFIG_DIR`/`DATA_DIR`; `sandbox_lark_broker_active()` (TTL-cached provisioner `/api/capabilities` probe, tight timeout + longer negative caching on the bash hot path) selects broker vs. binary mode for both the bash env overlay and status. `DEER_FLOW_LARK_CLI_SANDBOX_RUNTIME_DIR` supplies a validated, symlink-free pre-staged runtime for air-gapped deployments. For the remote provisioner (K8s), the runtime binary is otherwise provisioned by an optional init container + shared `emptyDir` (Pattern A): set `LARK_CLI_INIT_IMAGE` on the provisioner (see `docker/lark-cli-init/`) and the Gateway sends `provision_lark_cli_runtime` on sandbox create once the pack is installed, so remote installs skip the Gateway-side GitHub download entirely. Broker (Pattern B) supersedes the init-container binary (Pattern A) when both images are configured. `get_lark_integration_status(check_runtime=True)` surfaces `sandbox_runtime_mode` (`none` / `gateway-download` / `init-container` / `broker`) and `sandbox_runtime_ready` (remote modes read the provisioner `GET /api/capabilities`: `lark_cli_init_image` / `lark_cli_broker_image`) so a green UI can't hide a chat-time `lark-cli: command not found`. Cheap status probes are explicitly not live-verified; users authorize or reconnect through the browser device-flow endpoints instead of running terminal commands.
- **SkillScan**: `packages/harness/deerflow/skills/skillscan/` is the native deterministic scanner for `.skill` archives and agent-managed skill writes. It runs offline before the LLM scanner, emits structured findings (`rule_id`, `severity`, `file`, `line`, `message`, `remediation`, redacted `evidence` — category/analyzer are encoded in the `rule_id` prefix), blocks `CRITICAL`, and passes warning findings into `scan_skill_content()`. `scan_archive_preflight()` / `scan_skill_dir()` are pure sync functions (dispatch off the event loop); `enforce_static_scan()` applies the blocking policy and the `skill_scan.enabled` kill switch. The Python instance-client signal deliberately follows only a one-level, same-scope evidence chain (PR #4265 review): a proven imported constructor bound to a simple name, optional name-to-name alias propagation, rebinding invalidation, and a constructor-supported outbound method or context-manager use; bare canonical-looking names never fall back to module identity. Nested scopes never inherit client handles and inherit only constructor aliases proven stable by a binding-only enclosing-scope prepass. Comprehensions, walrus-bearing statements, annotations, executable expressions inside complex binding targets, unsupported operations, and ambiguous flows produce no finding from this signal; skipped constructs invalidate all names they may bind, while representative false negatives are pinned by `test_python_declared_false_negatives_stay_unreported`. Compound bodies are walked from isolated copies so wrapping code in `if True:` is not a bypass, while copied scope entries, binding-only prepasses, and AST visits consume a deterministic work budget and the walk stops after its first sink. Budget or recursion exhaustion skips only this best-effort signal and retains deterministic findings already collected for the file. Do not add Semgrep/OpenGrep or YAML rule-engine dependencies to the core path; Phase 1 rule specs live in Python constants next to their analyzers in `skillscan/orchestrator.py`. - **SkillScan**: `packages/harness/deerflow/skills/skillscan/` is the native deterministic scanner for `.skill` archives and agent-managed skill writes. It runs offline before the LLM scanner, emits structured findings (`rule_id`, `severity`, `file`, `line`, `message`, `remediation`, redacted `evidence` — category/analyzer are encoded in the `rule_id` prefix), blocks `CRITICAL`, and passes warning findings into `scan_skill_content()`. `scan_archive_preflight()` / `scan_skill_dir()` are pure sync functions (dispatch off the event loop); `enforce_static_scan()` applies the blocking policy and the `skill_scan.enabled` kill switch. The Python instance-client signal deliberately follows only a one-level, same-scope evidence chain (PR #4265 review): a proven imported constructor bound to a simple name, optional name-to-name alias propagation, rebinding invalidation, and a constructor-supported outbound method or context-manager use; bare canonical-looking names never fall back to module identity. Nested scopes never inherit client handles and inherit only constructor aliases proven stable by a binding-only enclosing-scope prepass. Comprehensions, walrus-bearing statements, annotations, executable expressions inside complex binding targets, unsupported operations, and ambiguous flows produce no finding from this signal; skipped constructs invalidate all names they may bind, while representative false negatives are pinned by `test_python_declared_false_negatives_stay_unreported`. Compound bodies are walked from isolated copies so wrapping code in `if True:` is not a bypass, while copied scope entries, binding-only prepasses, and AST visits consume a deterministic work budget and the walk stops after its first sink. Budget or recursion exhaustion skips only this best-effort signal and retains deterministic findings already collected for the file. Do not add Semgrep/OpenGrep or YAML rule-engine dependencies to the core path; Phase 1 rule specs live in Python constants next to their analyzers in `skillscan/orchestrator.py`.
- **Skill Review Core**: `packages/harness/deerflow/skills/review/` provides read-only package snapshots, deterministic facts, resource/eval analysis, report rendering, and the CLI (`python -m deerflow.skills.review.cli`). It reuses the shared frontmatter helper and SkillScan; it must not import `app.*`, execute target scripts, install dependencies, or call networks. JSON contracts live in `contracts/skill_review/`. The `review_skill_package` built-in tool labels results with `review_subject_entry` and never `skill_context_entry`, so reviewing a target does not activate it, bind its `required-secrets`, or apply its `allowed-tools`. Its model-visible `ToolMessage.content` is a compact JSON payload with untrusted control tags neutralized; the full raw review payload, including Markdown renders, stays in `ToolMessage.artifact`. CI should run the CLI with `--fail-on error --fail-on-incomplete` so blocker/error findings and truncated/not-assessed packages fail the gate. The public `skills/public/skill-reviewer` skill owns semantic readiness review and suggestions only; mutation and runtime experiments remain owned by `skill-creator`. - **Skill Review Core**: `packages/harness/deerflow/skills/review/` provides read-only package snapshots, deterministic facts, resource/eval analysis, report rendering, and the CLI (`python -m deerflow.skills.review.cli`). It reuses the shared frontmatter helper and SkillScan; it must not import `app.*`, execute target scripts, install dependencies, or call networks. JSON contracts live in `contracts/skill_review/`. The `review_skill_package` built-in tool labels results with `review_subject_entry` and never `skill_context_entry`, so reviewing a target does not activate it, bind its `required-secrets`, or apply its `allowed-tools`. Its model-visible `ToolMessage.content` is a compact JSON payload with untrusted control tags neutralized; the full raw review payload, including Markdown renders, stays in `ToolMessage.artifact`. CI should run the CLI with `--fail-on error --fail-on-incomplete` so blocker/error findings and truncated/not-assessed packages fail the gate. The public `skills/public/skill-reviewer` skill owns semantic readiness review and suggestions only; mutation and runtime experiments remain owned by `skill-creator`.

View File

@ -40,7 +40,7 @@ from deerflow.community.warm_pool_lifecycle import (
from deerflow.config import get_app_config from deerflow.config import get_app_config
from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths, join_host_path from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths, join_host_path
from deerflow.integrations.lark_cli import INTEGRATION_ID as LARK_CLI_INTEGRATION_ID from deerflow.integrations.lark_cli import INTEGRATION_ID as LARK_CLI_INTEGRATION_ID
from deerflow.integrations.lark_cli import LARK_CLI_SANDBOX_CONFIG_DIR, LARK_CLI_SANDBOX_DATA_DIR, LARK_CLI_SANDBOX_RUNTIME_DIR, ensure_lark_cli_credential_tree, lark_skills_installed from deerflow.integrations.lark_cli import LARK_CLI_SANDBOX_CONFIG_DIR, LARK_CLI_SANDBOX_DATA_DIR, LARK_CLI_SANDBOX_LOCKS_DIR, LARK_CLI_SANDBOX_RUNTIME_DIR, ensure_lark_cli_credential_tree, lark_skills_installed
from deerflow.runtime.user_context import get_effective_user_id from deerflow.runtime.user_context import get_effective_user_id
from deerflow.sandbox.sandbox import Sandbox from deerflow.sandbox.sandbox import Sandbox
from deerflow.sandbox.sandbox_provider import SandboxProvider from deerflow.sandbox.sandbox_provider import SandboxProvider
@ -1019,8 +1019,11 @@ class AioSandboxProvider(WarmPoolLifecycleMixin[SandboxInfo], SandboxProvider):
``lark-cli config init`` on the Gateway, never in-sandbox), so it is ``lark-cli config init`` on the Gateway, never in-sandbox), so it is
mounted **read-only**: sandbox processes only need to read it, and a mounted **read-only**: sandbox processes only need to read it, and a
read-only bind stops a compromised agent from tampering with or read-only bind stops a compromised agent from tampering with or
replacing the app credentials. The ``data`` dir holds refreshable OAuth replacing the app credentials. Newer ``lark-cli`` versions coordinate
tokens that ``lark-cli auth`` updates in-sandbox, so it stays writable. API calls through ``config/locks``, so that empty subdirectory is
over-mounted writable without exposing the rest of ``config`` to
writes. The ``data`` dir holds refreshable OAuth tokens that
``lark-cli auth`` updates in-sandbox, so it stays writable.
This is defense-in-depth only both dirs remain readable to arbitrary This is defense-in-depth only both dirs remain readable to arbitrary
sandbox processes until the auth-proxy follow-up (issue #4338) lands. sandbox processes until the auth-proxy follow-up (issue #4338) lands.
See the sandbox trust-boundary note in ``backend/AGENTS.md``. See the sandbox trust-boundary note in ``backend/AGENTS.md``.
@ -1029,8 +1032,10 @@ class AioSandboxProvider(WarmPoolLifecycleMixin[SandboxInfo], SandboxProvider):
paths = get_paths() paths = get_paths()
effective_user_id = AioSandboxProvider._effective_acquire_user_id(user_id) effective_user_id = AioSandboxProvider._effective_acquire_user_id(user_id)
ensure_lark_cli_credential_tree(effective_user_id, paths=paths) ensure_lark_cli_credential_tree(effective_user_id, paths=paths)
config_dir = paths.host_user_integration_config_dir(effective_user_id, LARK_CLI_INTEGRATION_ID)
mounts = [ mounts = [
(paths.host_user_integration_config_dir(effective_user_id, LARK_CLI_INTEGRATION_ID), LARK_CLI_SANDBOX_CONFIG_DIR, True), (config_dir, LARK_CLI_SANDBOX_CONFIG_DIR, True),
(join_host_path(config_dir, "locks"), LARK_CLI_SANDBOX_LOCKS_DIR, False),
(paths.host_user_integration_data_dir(effective_user_id, LARK_CLI_INTEGRATION_ID), LARK_CLI_SANDBOX_DATA_DIR, False), (paths.host_user_integration_data_dir(effective_user_id, LARK_CLI_INTEGRATION_ID), LARK_CLI_SANDBOX_DATA_DIR, False),
] ]
runtime_dir = paths.base_dir / "integrations" / LARK_CLI_INTEGRATION_ID / "sandbox-cli" runtime_dir = paths.base_dir / "integrations" / LARK_CLI_INTEGRATION_ID / "sandbox-cli"

View File

@ -34,6 +34,7 @@ _PROVISIONER_EXTRA_MOUNT_PATHS = {
"/mnt/skills/custom", "/mnt/skills/custom",
"/mnt/skills/integrations", "/mnt/skills/integrations",
"/mnt/integrations/lark-cli/config", "/mnt/integrations/lark-cli/config",
"/mnt/integrations/lark-cli/config/locks",
"/mnt/integrations/lark-cli/data", "/mnt/integrations/lark-cli/data",
"/mnt/integrations/lark-cli/runtime", "/mnt/integrations/lark-cli/runtime",
} }
@ -54,15 +55,17 @@ def _provisioner_extra_mounts_payload(
When ``provision_lark_cli_runtime`` is set, the provisioner supplies the When ``provision_lark_cli_runtime`` is set, the provisioner supplies the
lark-cli runtime via an init container + emptyDir, so the runtime extra mount lark-cli runtime via an init container + emptyDir, so the runtime extra mount
is dropped here to avoid a colliding hostPath/PVC mount at the same path. The is dropped here to avoid a colliding hostPath/PVC mount at the same path. The
per-user config/data credential mounts are still forwarded (they are mounted per-user config/locks/data mounts are still forwarded (they are mounted into
into the sandbox in Pattern A). the sandbox in Pattern A). The config root remains read-only while its
nested locks mount is writable for lark-cli's coordination files.
When ``provision_lark_cli_broker`` is set (Pattern B, issue #4338), the When ``provision_lark_cli_broker`` is set (Pattern B, issue #4338), the
provisioner runs a broker sidecar that holds the credentials, so the provisioner runs a broker sidecar that holds the credentials, so the
config/data mounts are **forwarded** (the provisioner wires them into the config/locks/data mounts are **forwarded** (the provisioner wires them into
sidecar, not the sandbox) while the runtime mount is dropped. Nothing changes the sidecar, not the sandbox) while the runtime mount is dropped. Nothing
in this payload beyond keeping config/data available for the provisioner to changes in this payload beyond keeping those credential-related mounts
place; the runtime entry is dropped in both modes. available for the provisioner to place; the runtime entry is dropped in
both modes.
""" """
if not extra_mounts: if not extra_mounts:
return [] return []

View File

@ -106,6 +106,7 @@ LARK_CLI_MAX_EXTRACTED_BYTES = 256 * 1024 * 1024
LARK_CLI_MAX_RUNTIME_ASSET_BYTES = 128 * 1024 * 1024 LARK_CLI_MAX_RUNTIME_ASSET_BYTES = 128 * 1024 * 1024
LARK_CLI_MANIFEST_FILE = ".deerflow-lark-cli-manifest.json" LARK_CLI_MANIFEST_FILE = ".deerflow-lark-cli-manifest.json"
LARK_CLI_SANDBOX_CONFIG_DIR = "/mnt/integrations/lark-cli/config" LARK_CLI_SANDBOX_CONFIG_DIR = "/mnt/integrations/lark-cli/config"
LARK_CLI_SANDBOX_LOCKS_DIR = f"{LARK_CLI_SANDBOX_CONFIG_DIR}/locks"
LARK_CLI_SANDBOX_DATA_DIR = "/mnt/integrations/lark-cli/data" LARK_CLI_SANDBOX_DATA_DIR = "/mnt/integrations/lark-cli/data"
LARK_CLI_SANDBOX_RUNTIME_DIR = "/mnt/integrations/lark-cli/runtime" LARK_CLI_SANDBOX_RUNTIME_DIR = "/mnt/integrations/lark-cli/runtime"
LARK_CLI_LINUX_ARCHES = ("amd64", "arm64") LARK_CLI_LINUX_ARCHES = ("amd64", "arm64")
@ -309,7 +310,7 @@ def ensure_lark_cli_credential_tree(user_id: str, *, paths: Paths | None = None)
raise ValueError(f"Lark CLI credential path must not be a symlink: {root}") raise ValueError(f"Lark CLI credential path must not be a symlink: {root}")
root.mkdir(parents=True, exist_ok=True, mode=0o700) root.mkdir(parents=True, exist_ok=True, mode=0o700)
root.chmod(0o700) root.chmod(0o700)
for required in (root / "config", root / "data"): for required in (root / "config", root / "config" / "locks", root / "data"):
if required.is_symlink(): if required.is_symlink():
raise ValueError(f"Lark CLI credential path must not be a symlink: {required}") raise ValueError(f"Lark CLI credential path must not be a symlink: {required}")
required.mkdir(parents=True, exist_ok=True, mode=0o700) required.mkdir(parents=True, exist_ok=True, mode=0o700)

View File

@ -183,17 +183,24 @@ def test_get_lark_cli_runtime_mounts_uses_user_auth_dirs(tmp_path, monkeypatch):
runtime_dir.mkdir(parents=True) runtime_dir.mkdir(parents=True)
mounts = aio_mod.AioSandboxProvider._get_lark_cli_runtime_mounts(user_id="alice") mounts = aio_mod.AioSandboxProvider._get_lark_cli_runtime_mounts(user_id="alice")
mount_order = [container_path for _host_path, container_path, _read_only in mounts]
container_paths = {container_path: (host_path, read_only) for host_path, container_path, read_only in mounts} container_paths = {container_path: (host_path, read_only) for host_path, container_path, read_only in mounts}
assert container_paths[lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR] == ( assert container_paths[lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR] == (
str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config"), str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config"),
True, True,
) )
assert container_paths[f"{lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR}/locks"] == (
str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config" / "locks"),
False,
)
assert mount_order.index(lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR) < mount_order.index(lark_cli.LARK_CLI_SANDBOX_LOCKS_DIR)
assert container_paths[lark_cli.LARK_CLI_SANDBOX_DATA_DIR] == ( assert container_paths[lark_cli.LARK_CLI_SANDBOX_DATA_DIR] == (
str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data"), str(tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data"),
False, False,
) )
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config").stat().st_mode) == 0o700 assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config").stat().st_mode) == 0o700
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "config" / "locks").stat().st_mode) == 0o700
assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data").stat().st_mode) == 0o700 assert stat.S_IMODE((tmp_path / "users" / "alice" / "integrations" / "lark-cli" / "data").stat().st_mode) == 0o700
assert container_paths["/mnt/integrations/lark-cli/runtime"] == ( assert container_paths["/mnt/integrations/lark-cli/runtime"] == (
str(runtime_dir), str(runtime_dir),
@ -254,12 +261,14 @@ def test_get_extra_mounts_provisioner_payload_has_unique_container_paths(tmp_pat
assert "/mnt/skills/custom" in container_paths assert "/mnt/skills/custom" in container_paths
assert "/mnt/skills/integrations" in container_paths assert "/mnt/skills/integrations" in container_paths
assert lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR in container_paths assert lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR in container_paths
assert lark_cli.LARK_CLI_SANDBOX_LOCKS_DIR in container_paths
assert lark_cli.LARK_CLI_SANDBOX_DATA_DIR in container_paths assert lark_cli.LARK_CLI_SANDBOX_DATA_DIR in container_paths
assert lark_cli.LARK_CLI_SANDBOX_RUNTIME_DIR in container_paths assert lark_cli.LARK_CLI_SANDBOX_RUNTIME_DIR in container_paths
payload = remote_backend._provisioner_extra_mounts_payload(mounts) payload = remote_backend._provisioner_extra_mounts_payload(mounts)
payload_paths = [str(item["container_path"]) for item in payload] payload_paths = [str(item["container_path"]) for item in payload]
assert len(payload_paths) == len(set(payload_paths)) assert len(payload_paths) == len(set(payload_paths))
assert payload_paths.index(lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR) < payload_paths.index(lark_cli.LARK_CLI_SANDBOX_LOCKS_DIR)
provisioner_module.DEER_FLOW_HOST_BASE_DIR = str(home) provisioner_module.DEER_FLOW_HOST_BASE_DIR = str(home)
validated = provisioner_module._validated_extra_mounts([provisioner_module.ExtraMount(**item) for item in payload]) validated = provisioner_module._validated_extra_mounts([provisioner_module.ExtraMount(**item) for item in payload])
@ -271,6 +280,7 @@ def test_get_extra_mounts_provisioner_payload_has_unique_container_paths(tmp_pat
"/mnt/skills/custom", "/mnt/skills/custom",
"/mnt/skills/integrations", "/mnt/skills/integrations",
lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR, lark_cli.LARK_CLI_SANDBOX_CONFIG_DIR,
lark_cli.LARK_CLI_SANDBOX_LOCKS_DIR,
lark_cli.LARK_CLI_SANDBOX_DATA_DIR, lark_cli.LARK_CLI_SANDBOX_DATA_DIR,
lark_cli.LARK_CLI_SANDBOX_RUNTIME_DIR, lark_cli.LARK_CLI_SANDBOX_RUNTIME_DIR,
} }

View File

@ -1073,6 +1073,7 @@ def test_lark_cli_env_hardens_existing_credential_tree(monkeypatch, tmp_path) ->
lark_cli.lark_cli_env_overlay("alice") lark_cli.lark_cli_env_overlay("alice")
assert stat.S_IMODE(config_dir.stat().st_mode) == 0o700 assert stat.S_IMODE(config_dir.stat().st_mode) == 0o700
assert stat.S_IMODE((config_dir / "locks").stat().st_mode) == 0o700
assert stat.S_IMODE(data_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(secret_file.stat().st_mode) == 0o600
assert stat.S_IMODE(token_file.stat().st_mode) == 0o600 assert stat.S_IMODE(token_file.stat().st_mode) == 0o600

View File

@ -25,4 +25,5 @@ def test_gateway_and_provisioner_extra_mount_contracts_match() -> None:
assert gateway_paths == provisioner_paths assert gateway_paths == provisioner_paths
assert "/mnt/integrations/lark-cli/runtime" in gateway_paths assert "/mnt/integrations/lark-cli/runtime" in gateway_paths
assert _literal_assignment(provisioner_path, "MAX_EXTRA_MOUNTS") == 9 assert "/mnt/integrations/lark-cli/config/locks" in gateway_paths
assert _literal_assignment(provisioner_path, "MAX_EXTRA_MOUNTS") == 10

View File

@ -538,6 +538,11 @@ class TestLarkCliInitContainer:
provisioner_module.ExtraMount( provisioner_module.ExtraMount(
host_path="/state/users/alice/integrations/lark-cli/config", host_path="/state/users/alice/integrations/lark-cli/config",
container_path="/mnt/integrations/lark-cli/config", container_path="/mnt/integrations/lark-cli/config",
read_only=True,
),
provisioner_module.ExtraMount(
host_path="/state/users/alice/integrations/lark-cli/config/locks",
container_path="/mnt/integrations/lark-cli/config/locks",
read_only=False, read_only=False,
), ),
provisioner_module.ExtraMount( provisioner_module.ExtraMount(
@ -555,14 +560,18 @@ class TestLarkCliInitContainer:
provision_lark_cli_runtime=True, provision_lark_cli_runtime=True,
) )
# The credential config mount stays; the hostPath runtime extra mount is # The read-only credential config and nested writable locks mounts stay;
# replaced by the emptyDir supplied by the init container (so the runtime # the hostPath runtime extra mount is replaced by the emptyDir supplied
# path is not backed by an extra-* hostPath volume). # by the init container (so the runtime path is not backed by an extra-*
# hostPath volume).
runtime_mounts = [m for m in pod.spec.containers[0].volume_mounts if m.mount_path == "/mnt/integrations/lark-cli/runtime"] runtime_mounts = [m for m in pod.spec.containers[0].volume_mounts if m.mount_path == "/mnt/integrations/lark-cli/runtime"]
assert len(runtime_mounts) == 1 assert len(runtime_mounts) == 1
assert runtime_mounts[0].name == provisioner_module.LARK_CLI_RUNTIME_VOLUME_NAME assert runtime_mounts[0].name == provisioner_module.LARK_CLI_RUNTIME_VOLUME_NAME
mount_paths = {m.mount_path for m in pod.spec.containers[0].volume_mounts} sandbox_mount_order = [m.mount_path for m in pod.spec.containers[0].volume_mounts]
assert "/mnt/integrations/lark-cli/config" in mount_paths sandbox_mounts = {m.mount_path: m for m in pod.spec.containers[0].volume_mounts}
assert sandbox_mounts["/mnt/integrations/lark-cli/config"].read_only is True
assert sandbox_mounts["/mnt/integrations/lark-cli/config/locks"].read_only is False
assert sandbox_mount_order.index("/mnt/integrations/lark-cli/config") < sandbox_mount_order.index("/mnt/integrations/lark-cli/config/locks")
class TestLarkCliBrokerSidecar: class TestLarkCliBrokerSidecar:
@ -576,6 +585,11 @@ class TestLarkCliBrokerSidecar:
container_path="/mnt/integrations/lark-cli/config", container_path="/mnt/integrations/lark-cli/config",
read_only=True, read_only=True,
), ),
provisioner_module.ExtraMount(
host_path="/state/users/alice/integrations/lark-cli/config/locks",
container_path="/mnt/integrations/lark-cli/config/locks",
read_only=False,
),
provisioner_module.ExtraMount( provisioner_module.ExtraMount(
host_path="/state/users/alice/integrations/lark-cli/data", host_path="/state/users/alice/integrations/lark-cli/data",
container_path="/mnt/integrations/lark-cli/data", container_path="/mnt/integrations/lark-cli/data",
@ -635,15 +649,22 @@ class TestLarkCliBrokerSidecar:
assert sidecar.image == "deer-flow/lark-cli-broker:v1.0.65" assert sidecar.image == "deer-flow/lark-cli-broker:v1.0.65"
assert sidecar.args == ["serve"] assert sidecar.args == ["serve"]
# Credentials mounted into the sidecar only. # Credentials mounted into the sidecar only.
sidecar_paths = {m.mount_path for m in sidecar.volume_mounts} sidecar_mount_order = [m.mount_path for m in sidecar.volume_mounts]
sidecar_mounts = {m.mount_path: m for m in sidecar.volume_mounts}
sidecar_paths = set(sidecar_mounts)
assert provisioner_module.LARK_BROKER_SIDECAR_CONFIG_PATH in sidecar_paths assert provisioner_module.LARK_BROKER_SIDECAR_CONFIG_PATH in sidecar_paths
assert provisioner_module.LARK_BROKER_SIDECAR_LOCKS_PATH in sidecar_paths
assert provisioner_module.LARK_BROKER_SIDECAR_DATA_PATH in sidecar_paths assert provisioner_module.LARK_BROKER_SIDECAR_DATA_PATH in sidecar_paths
assert sidecar_mounts[provisioner_module.LARK_BROKER_SIDECAR_CONFIG_PATH].read_only is True
assert sidecar_mounts[provisioner_module.LARK_BROKER_SIDECAR_LOCKS_PATH].read_only is False
assert sidecar_mount_order.index(provisioner_module.LARK_BROKER_SIDECAR_CONFIG_PATH) < sidecar_mount_order.index(provisioner_module.LARK_BROKER_SIDECAR_LOCKS_PATH)
# Sandbox container: runtime shim mount + broker URL env, NO config/data. # Sandbox container: runtime shim mount + broker URL env, NO config/data.
sandbox = pod.spec.containers[0] sandbox = pod.spec.containers[0]
sandbox_paths = {m.mount_path for m in sandbox.volume_mounts} sandbox_paths = {m.mount_path for m in sandbox.volume_mounts}
assert provisioner_module.LARK_CLI_RUNTIME_CONTAINER_PATH in sandbox_paths assert provisioner_module.LARK_CLI_RUNTIME_CONTAINER_PATH in sandbox_paths
assert "/mnt/integrations/lark-cli/config" not in sandbox_paths assert "/mnt/integrations/lark-cli/config" not in sandbox_paths
assert "/mnt/integrations/lark-cli/config/locks" not in sandbox_paths
assert "/mnt/integrations/lark-cli/data" not in sandbox_paths assert "/mnt/integrations/lark-cli/data" not in sandbox_paths
env = {e.name: e.value for e in (sandbox.env or [])} env = {e.name: e.value for e in (sandbox.env or [])}
assert env.get("DEERFLOW_LARK_BROKER_URL") == provisioner_module.LARK_BROKER_URL assert env.get("DEERFLOW_LARK_BROKER_URL") == provisioner_module.LARK_BROKER_URL

View File

@ -70,19 +70,23 @@ LARK_CLI_RUNTIME_VOLUME_NAME = "lark-cli-runtime"
# Optional "lark-cli broker" image (Pattern B, issue #4338). When set, sandbox # Optional "lark-cli broker" image (Pattern B, issue #4338). When set, sandbox
# Pods requesting the broker get an init container that stages a shim + a # Pods requesting the broker get an init container that stages a shim + a
# long-running broker sidecar that holds the credentials, instead of mounting the # long-running broker sidecar that holds the credentials, instead of mounting the
# plaintext config/data credential dirs into the sandbox container. Empty ⇒ broker # plaintext config/locks/data credential dirs into the sandbox container. Empty ⇒
# off (Pattern A / legacy behavior). Broker supersedes Pattern A when both are set. # broker off (Pattern A / legacy behavior). Broker supersedes Pattern A when both
# are set.
LARK_CLI_BROKER_IMAGE = os.environ.get("LARK_CLI_BROKER_IMAGE", "") LARK_CLI_BROKER_IMAGE = os.environ.get("LARK_CLI_BROKER_IMAGE", "")
# Optional comma-separated lark-cli subcommand denylist forwarded to the broker # Optional comma-separated lark-cli subcommand denylist forwarded to the broker
# sidecar (issue #4338 hardening). Empty ⇒ no subcommand is blocked. See the # sidecar (issue #4338 hardening). Empty ⇒ no subcommand is blocked. See the
# broker README's "subcommand denylist" section. # broker README's "subcommand denylist" section.
LARK_CLI_BROKER_DENY_SUBCOMMANDS = os.environ.get("DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS", "") LARK_CLI_BROKER_DENY_SUBCOMMANDS = os.environ.get("DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS", "")
LARK_CLI_CONFIG_CONTAINER_PATH = "/mnt/integrations/lark-cli/config" LARK_CLI_CONFIG_CONTAINER_PATH = "/mnt/integrations/lark-cli/config"
LARK_CLI_LOCKS_CONTAINER_PATH = f"{LARK_CLI_CONFIG_CONTAINER_PATH}/locks"
LARK_CLI_DATA_CONTAINER_PATH = "/mnt/integrations/lark-cli/data" LARK_CLI_DATA_CONTAINER_PATH = "/mnt/integrations/lark-cli/data"
# Where the broker sidecar reads the per-user credentials (sidecar-only paths). # Where the broker sidecar reads the per-user credentials (sidecar-only paths).
LARK_BROKER_SIDECAR_CONFIG_PATH = "/var/lark/config" LARK_BROKER_SIDECAR_CONFIG_PATH = "/var/lark/config"
LARK_BROKER_SIDECAR_LOCKS_PATH = f"{LARK_BROKER_SIDECAR_CONFIG_PATH}/locks"
LARK_BROKER_SIDECAR_DATA_PATH = "/var/lark/data" LARK_BROKER_SIDECAR_DATA_PATH = "/var/lark/data"
LARK_BROKER_CONFIG_VOLUME_NAME = "lark-cli-config" LARK_BROKER_CONFIG_VOLUME_NAME = "lark-cli-config"
LARK_BROKER_LOCKS_VOLUME_NAME = "lark-cli-locks"
LARK_BROKER_DATA_VOLUME_NAME = "lark-cli-data" LARK_BROKER_DATA_VOLUME_NAME = "lark-cli-data"
LARK_BROKER_URL = "http://127.0.0.1:8788" LARK_BROKER_URL = "http://127.0.0.1:8788"
THREADS_HOST_PATH = os.environ.get("THREADS_HOST_PATH", "/.deer-flow/threads") THREADS_HOST_PATH = os.environ.get("THREADS_HOST_PATH", "/.deer-flow/threads")
@ -103,12 +107,13 @@ if SANDBOX_SERVICE_TYPE not in {"NodePort", "ClusterIP"}:
SAFE_THREAD_ID_PATTERN = r"^[A-Za-z0-9_-]{1,64}$" SAFE_THREAD_ID_PATTERN = r"^[A-Za-z0-9_-]{1,64}$"
SAFE_USER_ID_PATTERN = r"^[A-Za-z0-9_\-]+$" SAFE_USER_ID_PATTERN = r"^[A-Za-z0-9_\-]+$"
DEFAULT_USER_ID = "default" DEFAULT_USER_ID = "default"
MAX_EXTRA_MOUNTS = 9 MAX_EXTRA_MOUNTS = 10
ALLOWED_EXTRA_MOUNT_PATHS = { ALLOWED_EXTRA_MOUNT_PATHS = {
"/mnt/acp-workspace", "/mnt/acp-workspace",
"/mnt/skills/custom", "/mnt/skills/custom",
"/mnt/skills/integrations", "/mnt/skills/integrations",
"/mnt/integrations/lark-cli/config", "/mnt/integrations/lark-cli/config",
"/mnt/integrations/lark-cli/config/locks",
"/mnt/integrations/lark-cli/data", "/mnt/integrations/lark-cli/data",
"/mnt/integrations/lark-cli/runtime", "/mnt/integrations/lark-cli/runtime",
} }
@ -231,18 +236,20 @@ def _runtime_provided_extra_mounts(
Pattern A (init container + emptyDir) provides Pattern A (init container + emptyDir) provides
``/mnt/integrations/lark-cli/runtime``, so a hostPath/PVC mount at the same ``/mnt/integrations/lark-cli/runtime``, so a hostPath/PVC mount at the same
path would collide it is dropped, leaving the per-user ``config`` / ``data`` path would collide it is dropped, leaving the per-user ``config`` /
credential mounts intact. ``config/locks`` / ``data`` mounts intact. The nested locks mount is writable
so lark-cli can coordinate API calls while the config root remains read-only.
Pattern B (broker sidecar) additionally moves the ``config`` / ``data`` Pattern B (broker sidecar) additionally moves all three mounts off the
credential mounts off the *sandbox* container and into the sidecar, so those *sandbox* container and into the sidecar, so those are dropped here too
are dropped here too the sandbox never sees plaintext credentials. the sandbox never sees plaintext credentials.
""" """
dropped: set[str] = set() dropped: set[str] = set()
if _lark_cli_broker_enabled(provision_lark_cli_broker): if _lark_cli_broker_enabled(provision_lark_cli_broker):
dropped = { dropped = {
LARK_CLI_RUNTIME_CONTAINER_PATH, LARK_CLI_RUNTIME_CONTAINER_PATH,
LARK_CLI_CONFIG_CONTAINER_PATH, LARK_CLI_CONFIG_CONTAINER_PATH,
LARK_CLI_LOCKS_CONTAINER_PATH,
LARK_CLI_DATA_CONTAINER_PATH, LARK_CLI_DATA_CONTAINER_PATH,
} }
elif _lark_cli_runtime_enabled(provision_lark_cli_runtime): elif _lark_cli_runtime_enabled(provision_lark_cli_runtime):
@ -253,15 +260,19 @@ def _runtime_provided_extra_mounts(
def _lark_broker_credential_mounts(extra_mounts: list["ExtraMount"] | None) -> dict[str, "ExtraMount"]: def _lark_broker_credential_mounts(extra_mounts: list["ExtraMount"] | None) -> dict[str, "ExtraMount"]:
"""Extract the config/data credential mounts the broker sidecar needs. """Extract the config/locks/data mounts the broker sidecar needs.
Keyed by container path so the caller can wire each into the sidecar's fixed Keyed by container path so the caller can wire each into the sidecar's fixed
``/var/lark/{config,data}`` paths. ``/var/lark/{config,config/locks,data}`` paths.
""" """
result: dict[str, ExtraMount] = {} result: dict[str, ExtraMount] = {}
for mount in _validated_extra_mounts(extra_mounts): for mount in _validated_extra_mounts(extra_mounts):
normalized = posixpath.normpath(mount.container_path) normalized = posixpath.normpath(mount.container_path)
if normalized in (LARK_CLI_CONFIG_CONTAINER_PATH, LARK_CLI_DATA_CONTAINER_PATH): if normalized in (
LARK_CLI_CONFIG_CONTAINER_PATH,
LARK_CLI_LOCKS_CONTAINER_PATH,
LARK_CLI_DATA_CONTAINER_PATH,
):
result[normalized] = mount result[normalized] = mount
return result return result
@ -590,11 +601,12 @@ def _build_volumes(
empty_dir=k8s_client.V1EmptyDirVolumeSource(), empty_dir=k8s_client.V1EmptyDirVolumeSource(),
) )
) )
# Pattern B: the config/data credential volumes go to the broker sidecar only. # Pattern B: config/locks/data volumes go to the broker sidecar only.
if _lark_cli_broker_enabled(provision_lark_cli_broker): if _lark_cli_broker_enabled(provision_lark_cli_broker):
credential_mounts = _lark_broker_credential_mounts(extra_mounts) credential_mounts = _lark_broker_credential_mounts(extra_mounts)
for container_path, volume_name in ( for container_path, volume_name in (
(LARK_CLI_CONFIG_CONTAINER_PATH, LARK_BROKER_CONFIG_VOLUME_NAME), (LARK_CLI_CONFIG_CONTAINER_PATH, LARK_BROKER_CONFIG_VOLUME_NAME),
(LARK_CLI_LOCKS_CONTAINER_PATH, LARK_BROKER_LOCKS_VOLUME_NAME),
(LARK_CLI_DATA_CONTAINER_PATH, LARK_BROKER_DATA_VOLUME_NAME), (LARK_CLI_DATA_CONTAINER_PATH, LARK_BROKER_DATA_VOLUME_NAME),
): ):
mount = credential_mounts.get(container_path) mount = credential_mounts.get(container_path)
@ -757,9 +769,10 @@ def _build_lark_cli_broker_sidecars(
) -> list[k8s_client.V1Container]: ) -> list[k8s_client.V1Container]:
"""Broker sidecar that holds lark-cli + the per-user credentials (Pattern B). """Broker sidecar that holds lark-cli + the per-user credentials (Pattern B).
The config/data credential dirs are mounted **only** here (never on the The config/locks/data dirs are mounted **only** here (never on the sandbox
sandbox container), so the plaintext app secret / OAuth tokens stay out of container), so the plaintext app secret / OAuth tokens stay out of the
the sandbox filesystem. The broker serves the command surface on loopback. sandbox filesystem. The config root is read-only and its nested locks mount
is writable. The broker serves the command surface on loopback.
""" """
if not _lark_cli_broker_enabled(provision_lark_cli_broker): if not _lark_cli_broker_enabled(provision_lark_cli_broker):
return [] return []
@ -767,6 +780,7 @@ def _build_lark_cli_broker_sidecars(
volume_mounts: list[k8s_client.V1VolumeMount] = [] volume_mounts: list[k8s_client.V1VolumeMount] = []
for container_path, volume_name, sidecar_path in ( for container_path, volume_name, sidecar_path in (
(LARK_CLI_CONFIG_CONTAINER_PATH, LARK_BROKER_CONFIG_VOLUME_NAME, LARK_BROKER_SIDECAR_CONFIG_PATH), (LARK_CLI_CONFIG_CONTAINER_PATH, LARK_BROKER_CONFIG_VOLUME_NAME, LARK_BROKER_SIDECAR_CONFIG_PATH),
(LARK_CLI_LOCKS_CONTAINER_PATH, LARK_BROKER_LOCKS_VOLUME_NAME, LARK_BROKER_SIDECAR_LOCKS_PATH),
(LARK_CLI_DATA_CONTAINER_PATH, LARK_BROKER_DATA_VOLUME_NAME, LARK_BROKER_SIDECAR_DATA_PATH), (LARK_CLI_DATA_CONTAINER_PATH, LARK_BROKER_DATA_VOLUME_NAME, LARK_BROKER_SIDECAR_DATA_PATH),
): ):
mount = credential_mounts.get(container_path) mount = credential_mounts.get(container_path)