Background
jiuwenclaw is a Chinese-origin multi-agent framework built on top of the openjiuwen agent-core. It supports a "team mode" where a leader agent coordinates a group of specialist teammates, each with their own skills, tools, and workspace.
This post documents three connected bugs we found and fixed, all triggered by a single user request: "Can the agent run shell commands the way Gemini CLI does?"
Problem 1: The React Loop (Agent Generates Text Instead of Tool Calls)
Symptom
After asking a question that required inspecting the machine (e.g. "what WiFi networks can you see?"), the agent entered an infinite loop. Each iteration the LLM produced a natural-language response describing how it would run a command — but never actually ran one. The runner log showed finish_reason: stop on every turn, meaning no tool was ever called.
Root Cause
The react loop in the openjiuwen harness re-invokes the LLM when the response has no tool call (finish_reason != tool_calls). The LLM was generating text because no shell tool existed in the agent's tool card list.
The mcp_exec_command tool was already implemented in command_tools.py and wired into the SkillDevService, but the team/agent mode uses a different tool registration path. In interface_deep.py, _get_tool_cards() built the list of available tools — and mcp_exec_command was never added to it.
Fix
Register mcp_exec_command in _get_tool_cards():
# jiuwenclaw/agentserver/deep_agent/interface_deep.py
from jiuwenclaw.agentserver.tools.command_tools import mcp_exec_command
def _get_tool_cards(self, ...) -> list[ToolCard]:
...
# After wiki tools:
if not Runner.resource_mgr.get_tool(mcp_exec_command.card.id):
Runner.resource_mgr.add_tool(mcp_exec_command)
tool_cards.append(mcp_exec_command.card)
return tool_cards
This is the pattern used by all other tools in that method. Once registered, the LLM can actually call the tool instead of narrating about it.
Problem 2: The Sandbox Restriction Blocked Useful Commands
Symptom
After fixing the registration, agents could call mcp_exec_command — but any workdir outside the agent workspace raised a ValueError, producing [ERROR]: workdir is outside project workspace. This prevented running commands like ip addr in /tmp or inspecting files anywhere outside the agent's sandbox.
Root Cause
_resolve_command_workdir unconditionally called:
candidate.relative_to(project_root) # raises ValueError if outside workspace
This was appropriate for a sandboxed skill dev environment but too restrictive for an agent with system-level access.
Fix
Introduce a MCP_EXEC_COMMAND_SANDBOX environment variable (default false) that opts into the restriction rather than enforcing it always:
def _is_workdir_sandbox_enabled() -> bool:
raw = os.getenv("MCP_EXEC_COMMAND_SANDBOX", "false").strip().lower()
return raw in ("1", "true", "yes", "on")
def _resolve_command_workdir(workdir: str) -> Path:
project_root = get_agent_workspace_dir()
candidate = Path(workdir) if workdir else project_root
if not candidate.is_absolute():
candidate = project_root / candidate
candidate = candidate.resolve()
if _is_workdir_sandbox_enabled():
candidate.relative_to(project_root) # raises ValueError if outside workspace
return candidate
The switch is documented in .env:
# mcp_exec_command: restrict workdir to agent workspace (true) or allow any path (false)
MCP_EXEC_COMMAND_SANDBOX=false
Problem 3: Workspace Files Written in Chinese Despite preferred_language: en
Symptom
After the agent's workspace was initialised, the generated files (AGENT.md, SOUL.md, HEARTBEAT.md, IDENTITY.md) were written in Chinese, even though the config contained preferred_language: en.
Root Cause — Tracing the Data Flow
The language setting travels through three layers before it reaches the template selector:
- Config →
preferred_language: en load_team_spec_dict→ builds per-agentWorkspaceSpecdictsWorkspaceSpec.language→ passed toget_workspace_schema(language)→ selects EN or CN template
The bug was in step 2. _DEFAULT_AGENT_WORKSPACE was defined as:
_DEFAULT_AGENT_WORKSPACE = {"stable_base": True}
No language key. When this dict was used to construct each agent's workspace spec via merged.setdefault("workspace", deepcopy(default_workspace)), the resulting spec had no language, so WorkspaceSpec.language fell back to its model default — "cn".
The preferred_language value was read in load_team_spec_dict but only assigned to spec_dict["language"] (the top-level field) — it was never propagated into the per-agent workspace dicts.
Fix
Thread preferred_language as a language parameter through _build_agents_config, then inject it into every workspace spec via setdefault (respecting any explicit per-agent override):
def _build_agents_config(
team_raw: dict[str, Any], config_base: dict[str, Any], language: str = "zh"
) -> dict[str, Any]:
default_model = _build_default_model_dict(config_base)
default_workspace, max_iterations, completion_timeout = _build_agent_defaults()
default_workspace.setdefault("language", language) # ← inject here
for agent_key, raw_agent_config in agents_raw.items():
agent_config = dict(raw_agent_config) if isinstance(raw_agent_config, dict) else {}
if "workspace" in agent_config and isinstance(agent_config["workspace"], dict):
agent_config["workspace"].setdefault("language", language) # ← and per-agent
...
Also propagate to the team-level workspace spec:
# in load_team_spec_dict:
resolved_lang = str(config_base.get("preferred_language", "zh")).strip().lower()
agents = _build_agents_config(team_raw, config_base, language=resolved_lang)
...
workspace_spec = _build_workspace_spec(team_raw)
if workspace_spec is not None:
workspace_spec.setdefault("language", resolved_lang)
spec_dict["workspace"] = workspace_spec
Using setdefault throughout means an agent that explicitly configures workspace: {language: zh} won't have it overridden.
Skill File
To guide the agent on what commands are available and how to use them, a skill was created at:
~/.jiuwenclaw/agent/jiuwenclaw_workspace/skills/system-commands/SKILL.md
The allowed_tools frontmatter field in jiuwenclaw's skill system is advisory — it hints to the LLM which tools the skill uses. Actual tool availability is determined by what's registered in _get_tool_cards(). The skill body includes common command patterns for networking, hardware inspection, process listing, and log tailing.
Unit Tests
test_command_tools.py — 19 tests
TestSandboxSwitch (3 tests): verifies _is_workdir_sandbox_enabled() reads the env var correctly, including case-insensitive truthy/falsy parsing for true/True/TRUE/1/yes/on and false/False/FALSE/0/no/off.
TestResolveCommandWorkdir (6 tests): verifies that:
- Empty workdir falls back to agent workspace
- Relative paths are resolved under the workspace
- Sandbox off: absolute paths outside workspace are allowed
- Sandbox on: absolute paths outside workspace raise ValueError
- Sandbox on: paths inside workspace (including root) are allowed
test_team_config_loader.py — 8 new tests in TestLanguagePropagation
preferred_language: ensetsspec["language"] == "en"at the top level- Absent
preferred_languagedefaults to"zh" "en"propagates to each agent's workspace spec"en"propagates to all agents when multiple agents are configured"en"is injected into explicit per-agent workspace dicts (that set other keys)- An explicit
workspace: {language: zh}on an agent is not overridden "en"propagates to the team-level workspace spec- An explicit
language: zhin the team workspace spec is not overridden
Key Architectural Learnings
-
_get_tool_cards()is the source of truth for tool availability in team/agent mode — notget_mcp_tools(), not skillallowed_tools. Any tool that should be callable must be explicitly added here. -
The react harness loops on
finish_reason != tool_calls— if the model can't call a tool (because none are registered for the task), it will narrate endlessly. The fix is always to provide the right tool, not to adjust the loop logic. -
setdefaultis the right pattern for propagating config defaults — it lets outer layers provide defaults while inner (explicit) config takes precedence. -
MCP_EXEC_COMMAND_SANDBOXfollows the same bool-string convention as other env vars —1/true/yes/onenable, everything else (including absence) disables.
Files Changed
| File | Change |
|---|---|
jiuwenclaw/agentserver/deep_agent/interface_deep.py |
Register mcp_exec_command in _get_tool_cards() |
jiuwenclaw/agentserver/tools/command_tools.py |
Add _is_workdir_sandbox_enabled(), make sandbox conditional |
jiuwenclaw/agentserver/team/config_loader.py |
Propagate preferred_language into all workspace specs |
tests/unit_tests/agentserver/test_command_tools.py |
New — 19 tests for sandbox switch and workdir resolution |
tests/unit_tests/agentserver/test_team_config_loader.py |
8 new TestLanguagePropagation tests + updated 1 existing assertion |
~/.jiuwenclaw/agent/.../skills/system-commands/SKILL.md |
New skill providing shell command guidance |
~/.jiuwenclaw/config/.env |
Document MCP_EXEC_COMMAND_SANDBOX=false |