LLM agent file_write calls are always blocked — safe_mode: false / context.global.unsafe: true never reach _unsafe_mode #115

Closed
opened 2026-08-05 18:19:24 +00:00 by CoreRasurae · 0 comments
Member

Metadata

  • Commit Message: fix(agents): honor safe_mode and context.global.unsafe for LLM-agent tool calls
  • Branch: bugfix/m1-llm-tool-unsafe-mode

Background and context

ADR-2030 added tool-calling support to type: llm agents (docs/adr/ADR-2030-tool-calling-spec-extensions.md), but never addressed how the pre-existing safe_mode / unsafe / _unsafe_mode contract (docs/index.md §4.5, §4.5.4, §9.4, §10.3) composes with LLM-agent-issued tool calls. Investigating why packages/calculator-app-actor.yaml (an LLM agent with safe_mode: false and context.global.unsafe: true) can never write its generated files surfaced two compounding implementation gaps, both in the tool-call dispatch path — not a spec ambiguity.

Current behavior

Every file_write call made by an LLM agent's tool-call loop fails, regardless of the actor's configuration:

Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode
Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode
Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode
Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode

Reproduction:

  1. Load packages/calculator-app-actor.yaml — a type: llm agent (calculator_builder) with tools: [file_read, file_write, shell], safe_mode: false, and an actor-level context.global.unsafe: true.
  2. Execute the actor's graph route (build node → calculator_builder) via cleveractors.runtime.create_executor.
  3. The model issues a file_write tool call.
  4. The call unconditionally raises ExecutionError("File writing requires unsafe mode").

Root cause — two compounding bugs:

  1. Dead config key in cleveractors.agents.llm.LLMAgent. All three tool-call dispatch sites in the multi-turn tool loop (_execute_tool_loop and its two budget/stuck-model synthesis-retry mirrors) compute:

    parent_unsafe = self.config.get("unsafe_mode", False)
    

    before building the ephemeral per-call ToolAgent and its context via LLMAgent._build_tool_context(parent_unsafe). No code anywhere in the codebase (cleveractors.agents.factory.AgentFactory, cleveractors.core.application, cleveractors.runtime_dispatch) ever sets config["unsafe_mode"] — it is a key nobody writes. The actual field an actor author sets is safe_mode (docs/index.md §4.5) — packages/calculator-app-actor.yaml sets safe_mode: false on calculator_builder, but the tool-call dispatch path never reads self.config.get("safe_mode", ...). parent_unsafe is therefore always False, _build_tool_context never includes _unsafe_mode, and cleveractors.agents.tool.ToolAgent._file_write_tool (§4.5.4) always raises.

  2. Runtime-layer gap for the unsafe host contract. Per §9.4/§10.3, context.global.unsafe: true on an actor should be checked against whether the host was actually placed in unsafe mode — refusing execution with UnsafeConfigurationError if not, and propagating _unsafe_mode: true into every invocation context if so. This is correctly implemented in cleveractors.core.application.Application._enforce_unsafe_flag (+ its metadata["_unsafe_mode"] = self.unsafe call sites). But cleveractors.runtime_dispatch._execute_graph / _execute_graph_stream — the path packages/*.yaml actors run through via cleveractors.runtime.create_executor — has no host-unsafe-flag concept at all, and LLMAgent._build_tool_context ignores the graph's global_context entirely when building the per-tool-call context.

Net effect: no LLM-agent-driven file_write can currently succeed through the Executor runtime path, under any actor configuration.

Expected behavior

  • An LLM agent with safe_mode: false can successfully write files via model-issued file_write tool calls.
  • An actor declaring context.global.unsafe: true, executed on a host actually placed in unsafe mode, propagates _unsafe_mode: true into every tool-call context reachable from the graph (§9.4/§10.3).
  • The same actor executed on a host not in unsafe mode is refused with UnsafeConfigurationError (§9.4) rather than surfacing a generic, always-on file_write ExecutionError.
  • Default (safe_mode: true) sandboxing behavior for file_read/shell is unaffected.

Acceptance criteria

  • An LLM agent (type: llm) with tools: [file_write] and safe_mode: false successfully executes a model-issued file_write tool call.
  • LLMAgent's tool-call dispatch derives parent_unsafe from the agent's own safe_mode config field, not the nonexistent unsafe_mode key.
  • Executing an actor via cleveractors.runtime.create_executor with context.global.unsafe: true declared, on a host placed in unsafe mode, results in _unsafe_mode: true reaching every tool-call context in the graph.
  • The same actor executed on a host not in unsafe mode raises UnsafeConfigurationError instead of a file_write ExecutionError.
  • No regression to default safe_mode: true restrictions on file_read/shell/directory traversal.

Supporting information

  • Spec references: docs/index.md §4.5 (Tool Agents, safe_mode field), §4.5.4 (Safe-Mode Restrictions), §9.4 (unsafe), §10.3 (_unsafe_mode reserved context key).
  • docs/adr/ADR-2030-tool-calling-spec-extensions.md — added LLM-agent tool calling but never addressed safe_mode/unsafe composition for it; a follow-up ADR revision may be warranted once the fix approach is settled.
  • Fixture that surfaces the bug: packages/calculator-app-actor.yaml.

Subtasks

  • Fix cleveractors.agents.llm.LLMAgent's three tool-call dispatch sites to derive parent_unsafe from self.config.get("safe_mode", True) is False, not the dead unsafe_mode key.
  • Wire the actor's context.global.unsafe declaration through cleveractors.runtime_dispatch._execute_graph / _execute_graph_stream into a host-unsafe-flag check (mirroring cleveractors.core.application.Application._enforce_unsafe_flag), raising UnsafeConfigurationError when required but absent, and propagating _unsafe_mode: true into every tool-call context when present.
  • Tests (Behave): scenarios covering both the LLM-agent safe_mode: false path and the context.global.unsafe host-mode path for file_write, including the UnsafeConfigurationError refusal case.
  • Tests (Robot): integration test exercising an LLM-agent actor with file_write end-to-end through the Executor graph path with an unsafe host.
  • Verify coverage >= 97% via nox -s coverage_report.
  • Run nox (all default sessions), fix any errors.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata - **Commit Message:** fix(agents): honor safe_mode and context.global.unsafe for LLM-agent tool calls - **Branch:** bugfix/m1-llm-tool-unsafe-mode ## Background and context `ADR-2030` added tool-calling support to `type: llm` agents (`docs/adr/ADR-2030-tool-calling-spec-extensions.md`), but never addressed how the pre-existing `safe_mode` / `unsafe` / `_unsafe_mode` contract (docs/index.md §4.5, §4.5.4, §9.4, §10.3) composes with LLM-agent-issued tool calls. Investigating why `packages/calculator-app-actor.yaml` (an LLM agent with `safe_mode: false` and `context.global.unsafe: true`) can never write its generated files surfaced two compounding implementation gaps, both in the tool-call dispatch path — not a spec ambiguity. ## Current behavior Every `file_write` call made by an LLM agent's tool-call loop fails, regardless of the actor's configuration: ``` Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode Tool 'file_write' error: Tool execution failed: File writing requires unsafe mode ``` **Reproduction:** 1. Load `packages/calculator-app-actor.yaml` — a `type: llm` agent (`calculator_builder`) with `tools: [file_read, file_write, shell]`, `safe_mode: false`, and an actor-level `context.global.unsafe: true`. 2. Execute the actor's graph route (`build` node → `calculator_builder`) via `cleveractors.runtime.create_executor`. 3. The model issues a `file_write` tool call. 4. The call unconditionally raises `ExecutionError("File writing requires unsafe mode")`. **Root cause — two compounding bugs:** 1. **Dead config key in `cleveractors.agents.llm.LLMAgent`.** All three tool-call dispatch sites in the multi-turn tool loop (`_execute_tool_loop` and its two budget/stuck-model synthesis-retry mirrors) compute: ```python parent_unsafe = self.config.get("unsafe_mode", False) ``` before building the ephemeral per-call `ToolAgent` and its context via `LLMAgent._build_tool_context(parent_unsafe)`. No code anywhere in the codebase (`cleveractors.agents.factory.AgentFactory`, `cleveractors.core.application`, `cleveractors.runtime_dispatch`) ever sets `config["unsafe_mode"]` — it is a key nobody writes. The actual field an actor author sets is `safe_mode` (docs/index.md §4.5) — `packages/calculator-app-actor.yaml` sets `safe_mode: false` on `calculator_builder`, but the tool-call dispatch path never reads `self.config.get("safe_mode", ...)`. `parent_unsafe` is therefore always `False`, `_build_tool_context` never includes `_unsafe_mode`, and `cleveractors.agents.tool.ToolAgent._file_write_tool` (§4.5.4) always raises. 2. **Runtime-layer gap for the `unsafe` host contract.** Per §9.4/§10.3, `context.global.unsafe: true` on an actor should be checked against whether the *host* was actually placed in unsafe mode — refusing execution with `UnsafeConfigurationError` if not, and propagating `_unsafe_mode: true` into every invocation context if so. This is correctly implemented in `cleveractors.core.application.Application._enforce_unsafe_flag` (+ its `metadata["_unsafe_mode"] = self.unsafe` call sites). But `cleveractors.runtime_dispatch._execute_graph` / `_execute_graph_stream` — the path `packages/*.yaml` actors run through via `cleveractors.runtime.create_executor` — has no host-unsafe-flag concept at all, and `LLMAgent._build_tool_context` ignores the graph's `global_context` entirely when building the per-tool-call context. Net effect: no LLM-agent-driven `file_write` can currently succeed through the `Executor` runtime path, under any actor configuration. ## Expected behavior - An LLM agent with `safe_mode: false` can successfully write files via model-issued `file_write` tool calls. - An actor declaring `context.global.unsafe: true`, executed on a host actually placed in unsafe mode, propagates `_unsafe_mode: true` into every tool-call context reachable from the graph (§9.4/§10.3). - The same actor executed on a host *not* in unsafe mode is refused with `UnsafeConfigurationError` (§9.4) rather than surfacing a generic, always-on `file_write` `ExecutionError`. - Default (`safe_mode: true`) sandboxing behavior for `file_read`/`shell` is unaffected. ## Acceptance criteria - [ ] An LLM agent (`type: llm`) with `tools: [file_write]` and `safe_mode: false` successfully executes a model-issued `file_write` tool call. - [ ] `LLMAgent`'s tool-call dispatch derives `parent_unsafe` from the agent's own `safe_mode` config field, not the nonexistent `unsafe_mode` key. - [ ] Executing an actor via `cleveractors.runtime.create_executor` with `context.global.unsafe: true` declared, on a host placed in unsafe mode, results in `_unsafe_mode: true` reaching every tool-call context in the graph. - [ ] The same actor executed on a host *not* in unsafe mode raises `UnsafeConfigurationError` instead of a `file_write` `ExecutionError`. - [ ] No regression to default `safe_mode: true` restrictions on `file_read`/`shell`/directory traversal. ## Supporting information - Spec references: `docs/index.md` §4.5 (Tool Agents, `safe_mode` field), §4.5.4 (Safe-Mode Restrictions), §9.4 (`unsafe`), §10.3 (`_unsafe_mode` reserved context key). - `docs/adr/ADR-2030-tool-calling-spec-extensions.md` — added LLM-agent tool calling but never addressed `safe_mode`/`unsafe` composition for it; a follow-up ADR revision may be warranted once the fix approach is settled. - Fixture that surfaces the bug: `packages/calculator-app-actor.yaml`. ## Subtasks - [ ] Fix `cleveractors.agents.llm.LLMAgent`'s three tool-call dispatch sites to derive `parent_unsafe` from `self.config.get("safe_mode", True) is False`, not the dead `unsafe_mode` key. - [ ] Wire the actor's `context.global.unsafe` declaration through `cleveractors.runtime_dispatch._execute_graph` / `_execute_graph_stream` into a host-unsafe-flag check (mirroring `cleveractors.core.application.Application._enforce_unsafe_flag`), raising `UnsafeConfigurationError` when required but absent, and propagating `_unsafe_mode: true` into every tool-call context when present. - [ ] Tests (Behave): scenarios covering both the LLM-agent `safe_mode: false` path and the `context.global.unsafe` host-mode path for `file_write`, including the `UnsafeConfigurationError` refusal case. - [ ] Tests (Robot): integration test exercising an LLM-agent actor with `file_write` end-to-end through the `Executor` graph path with an unsafe host. - [ ] Verify coverage >= 97% via `nox -s coverage_report`. - [ ] Run `nox` (all default sessions), fix any errors. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The commit is submitted as a PR to master, reviewed, and merged.
CoreRasurae added this to the v2.1.0 milestone 2026-08-09 23:31:32 +00:00
CoreRasurae 2026-08-20 17:32:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveractors-core#115
No description provided.