mirror of
https://github.com/langgenius/dify.git
synced 2026-06-24 13:01:16 +08:00
fix(agent): inject env inline per-command instead of persisting workspace file (#37815)
This commit is contained in:
parent
be3677f145
commit
4ac8c5a30e
@ -18,9 +18,11 @@ side-effecting ``on_context_resume`` attempt fails after issuing shellctl jobs,
|
||||
Agenton still exits ``resource_context()`` but never transitions the layer to
|
||||
``ACTIVE``. In that failed-enter path, normal suspend/delete hooks do not run,
|
||||
so the enter hook itself must perform best-effort business compensation before
|
||||
re-raising the failure. Agent Stub env injection uses shellctl's native per-run
|
||||
``env`` argument for user-visible ``shell.run`` and for trusted server-owned
|
||||
fixed scripts executed through ``run_remote_script()``.
|
||||
re-raising the failure. Agent Soul shell env is injected into user-visible
|
||||
commands and CLI bootstrap commands without persisting a workspace env file.
|
||||
Agent Stub env injection uses shellctl's native per-run ``env`` argument for
|
||||
user-visible ``shell.run`` and for trusted server-owned fixed scripts executed
|
||||
through ``run_remote_script()``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -475,7 +477,7 @@ class DifyShellLayer(PydanticAILayer[DifyShellLayerDeps, object, DifyShellLayerC
|
||||
try:
|
||||
client = self._require_client()
|
||||
result = await client.run(
|
||||
_wrap_user_script(script),
|
||||
_wrap_user_script(script, self.config),
|
||||
cwd=self._require_workspace_cwd(),
|
||||
env=self._build_user_shell_run_env(),
|
||||
timeout=timeout,
|
||||
@ -536,9 +538,9 @@ class DifyShellLayer(PydanticAILayer[DifyShellLayerDeps, object, DifyShellLayerC
|
||||
and optional Agent Stub env injection.
|
||||
|
||||
Unlike model-visible ``shell.run``, this server-owned boundary does not
|
||||
source ``.dify/env.sh``. That file is user-controlled shell config, so
|
||||
sourcing it here would let sandbox code clobber trusted Agent Stub env
|
||||
values before ``dify-agent file upload`` executes.
|
||||
inject Agent Soul shell env. Keeping the user-controlled shell env out
|
||||
of this path prevents sandbox code from clobbering trusted Agent Stub
|
||||
env values before ``dify-agent file upload`` executes.
|
||||
"""
|
||||
env = None
|
||||
if inject_agent_stub_env:
|
||||
@ -833,16 +835,18 @@ def _workspace_cwd(session_id: str) -> str:
|
||||
|
||||
|
||||
def _workspace_bootstrap_script(config: DifyShellLayerConfig) -> str:
|
||||
"""Return the workspace bootstrap script for env + CLI tool declarations."""
|
||||
has_bootstrap = bool(config.env or config.secret_refs or config.cli_tools or config.sandbox is not None)
|
||||
if not has_bootstrap:
|
||||
"""Return the workspace bootstrap script for CLI tool declarations."""
|
||||
install_commands = [command for tool in config.cli_tools for command in tool.install_commands]
|
||||
if not install_commands:
|
||||
return ""
|
||||
|
||||
lines: list[str] = [
|
||||
"set -eu",
|
||||
'mkdir -p ".dify"',
|
||||
"cat > \".dify/env.sh\" <<'DIFY_ENV_EOF'",
|
||||
]
|
||||
lines: list[str] = ["set -eu", *_shell_config_export_lines(config), *install_commands]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _shell_config_export_lines(config: DifyShellLayerConfig) -> list[str]:
|
||||
"""Return ephemeral Agent Soul shell exports for one shellctl command."""
|
||||
lines: list[str] = []
|
||||
for env_var in config.env:
|
||||
lines.append(f"export {env_var.name}={_shquote(env_var.value)}")
|
||||
for secret_ref in config.secret_refs:
|
||||
@ -860,32 +864,15 @@ def _workspace_bootstrap_script(config: DifyShellLayerConfig) -> str:
|
||||
if config.sandbox.config:
|
||||
sandbox_config = json.dumps(config.sandbox.config, ensure_ascii=True, sort_keys=True)
|
||||
lines.append(f"export DIFY_SANDBOX_CONFIG_JSON={_shquote(sandbox_config)}")
|
||||
lines.extend(
|
||||
[
|
||||
"DIFY_ENV_EOF",
|
||||
'chmod 600 ".dify/env.sh"',
|
||||
'. ".dify/env.sh"',
|
||||
]
|
||||
)
|
||||
for tool in config.cli_tools:
|
||||
for command in tool.install_commands:
|
||||
lines.append(command)
|
||||
return "\n".join(lines)
|
||||
return lines
|
||||
|
||||
|
||||
def _wrap_user_script(script: str) -> str:
|
||||
"""Source Agent Soul env before executing a model-requested shell command."""
|
||||
# TODO: refactor
|
||||
return "\n".join(
|
||||
[
|
||||
'if [ -f ".dify/env.sh" ]; then',
|
||||
" set -a",
|
||||
' . ".dify/env.sh"',
|
||||
" set +a",
|
||||
"fi",
|
||||
script,
|
||||
]
|
||||
)
|
||||
def _wrap_user_script(script: str, config: DifyShellLayerConfig) -> str:
|
||||
"""Inject Agent Soul env before executing a model-requested shell command."""
|
||||
lines = _shell_config_export_lines(config)
|
||||
if not lines:
|
||||
return script
|
||||
return "\n".join([*lines, script])
|
||||
|
||||
|
||||
def _workspace_mkdir_script(*, session_id: str) -> str:
|
||||
|
||||
@ -3,6 +3,7 @@ from collections.abc import Callable, Mapping
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
@ -454,7 +455,6 @@ def test_shell_layer_create_bootstraps_agent_soul_shell_config(monkeypatch: pyte
|
||||
assert 'export GITHUB_TOKEN="${GITHUB_TOKEN:-}"' in script
|
||||
assert "export DIFY_SANDBOX_PROVIDER='independent'" in script
|
||||
assert "export DIFY_SANDBOX_CONFIG_JSON='{\"cpu\": 2}'" in script
|
||||
assert '. ".dify/env.sh"' in script
|
||||
assert "apt-get install -y ripgrep" in script
|
||||
return _job_result("bootstrap-job", status=JobStatusName.EXITED, done=True, exit_code=0)
|
||||
|
||||
@ -489,10 +489,60 @@ def test_shell_layer_create_bootstraps_agent_soul_shell_config(monkeypatch: pyte
|
||||
assert layer.runtime_state.job_ids == ["mkdir-job", "bootstrap-job"]
|
||||
|
||||
|
||||
def test_shell_layer_injects_agent_soul_env_without_workspace_env_file(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(time, "time", lambda: 0xABC12)
|
||||
|
||||
def token_hex(_nbytes: int) -> str:
|
||||
return "ff"
|
||||
|
||||
monkeypatch.setattr(secrets, "token_hex", token_hex)
|
||||
|
||||
def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> JobResult:
|
||||
del timeout
|
||||
assert env is None
|
||||
if cwd is None:
|
||||
return _job_result("mkdir-job", status=JobStatusName.EXITED, done=True, exit_code=0)
|
||||
|
||||
assert cwd == "~/workspace/abc12ff"
|
||||
assert "export PROJECT_NAME='demo project'" in script
|
||||
assert 'export OPENAI_API_KEY="${OPENAI_API_KEY:-}"' in script
|
||||
assert "export DIFY_SANDBOX_PROVIDER='independent'" in script
|
||||
assert "export DIFY_SANDBOX_CONFIG_JSON='{\"cpu\": 2}'" in script
|
||||
assert script.endswith("\npwd")
|
||||
return _job_result("user-job", status=JobStatusName.EXITED, done=True, exit_code=0)
|
||||
|
||||
client = FakeShellctlClient(run_handler=run_handler)
|
||||
layer = _shell_layer(
|
||||
client_factory=lambda _entrypoint: client,
|
||||
config=DifyShellLayerConfig(
|
||||
env=[DifyShellEnvVarConfig(name="PROJECT_NAME", value="demo project")],
|
||||
secret_refs=[DifyShellSecretRefConfig(name="OPENAI_API_KEY", ref="secret-1")],
|
||||
sandbox=DifyShellSandboxConfig(provider="independent", config={"cpu": 2}),
|
||||
),
|
||||
)
|
||||
tools = {tool.name: tool for tool in layer.tools}
|
||||
|
||||
async def scenario() -> None:
|
||||
async with layer.resource_context():
|
||||
await layer.on_context_create()
|
||||
run_result = cast(
|
||||
Mapping[str, object],
|
||||
await tools["shell_run"].function_schema.call(
|
||||
{"script": "pwd"},
|
||||
None, # pyright: ignore[reportArgumentType]
|
||||
),
|
||||
)
|
||||
assert run_result["job_id"] == "user-job"
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
assert [call.cwd for call in client.run_calls] == [None, "~/workspace/abc12ff"]
|
||||
assert layer.runtime_state.job_ids == ["mkdir-job", "user-job"]
|
||||
|
||||
|
||||
def test_shell_layer_tools_map_inputs_to_shellctl_calls_and_maintain_offsets() -> None:
|
||||
def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> JobResult:
|
||||
assert script.endswith("\npwd")
|
||||
assert '. ".dify/env.sh"' in script
|
||||
assert script == "pwd"
|
||||
assert cwd == "~/workspace/abc12ff"
|
||||
assert env is None
|
||||
assert timeout == 2.5
|
||||
@ -608,8 +658,7 @@ def test_shell_layer_tools_map_inputs_to_shellctl_calls_and_maintain_offsets() -
|
||||
def test_shell_layer_injects_agent_stub_env_only_for_user_visible_shell_run() -> None:
|
||||
def run_handler(script: str, cwd: str | None, env: Mapping[str, str] | None, timeout: float) -> JobResult:
|
||||
del cwd, timeout
|
||||
if script.endswith("\npwd"):
|
||||
assert '. ".dify/env.sh"' in script
|
||||
if script == "pwd":
|
||||
assert env is not None
|
||||
return _job_result("user-job", status=JobStatusName.EXITED, done=True, exit_code=0)
|
||||
assert env is None
|
||||
@ -639,8 +688,8 @@ def test_shell_layer_injects_agent_stub_env_only_for_user_visible_shell_run() ->
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
user_run_call = next(call for call in client.run_calls if call.script.endswith("\npwd"))
|
||||
internal_run_calls = [call for call in client.run_calls if not call.script.endswith("\npwd")]
|
||||
user_run_call = next(call for call in client.run_calls if call.script == "pwd")
|
||||
internal_run_calls = [call for call in client.run_calls if call.script != "pwd"]
|
||||
|
||||
assert user_run_call.env == {
|
||||
AGENT_STUB_API_BASE_URL_ENV_VAR: "https://agent.example.com/agent-stub",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user