You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/14 01:19:03 UTC

(maka) 03/14: fix(agent): freeze editing policy and project native Code Mode results

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit aed4daad2d3d7d2ff45b671a8fdb16dda682bd7f
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Oct 13 16:49:54 2026 +0800

    fix(agent): freeze editing policy and project native Code Mode results
    
    Capture independent file-editing selection at admission and preserve it across
    queued work, continuations and Files-backed restart. Select editor methods by
    their bound contract, including skill tool metadata and renamed tools.
    
    Return declared business values to nested JavaScript after journaling original
    native receipts. Verify real writes, permissions, wait, cancellation and image
    bytes, and attest the Files owner's captured Root and private boundary.
    
    Validate native, engine, tools, facts, affected EventLog and product regressions,
    real CLI/TUI flows, and strict all-target Clippy on affected crates. Correct the
    production API dependency and existing CLI test checks found by verification.
---
 platform/event-log/tests/auxiliary_usage.rs        |   1 +
 platform/event-log/tests/continuation/fixtures.rs  |   1 +
 platform/event-log/tests/executor.rs               |   1 +
 .../engine/tests/integration/support/invocation.rs |   1 +
 plugins/agent/facts/src/execution.rs               |   3 +
 plugins/agent/facts/src/tool_output.rs             |  17 +-
 plugins/agent/native/Cargo.toml                    |   2 +-
 plugins/agent/native/README.md                     |   4 +-
 plugins/agent/native/README.zh-CN.md               |   4 +-
 plugins/agent/native/src/model.rs                  |  12 +
 plugins/agent/native/src/owner.rs                  |  44 +-
 plugins/agent/native/src/owner/execution.rs        |   2 +
 plugins/agent/native/src/owner/queue.rs            |  10 +-
 plugins/agent/native/src/owner/tests.rs            |  21 +-
 plugins/agent/native/src/owner/tests/files.rs      |  12 +
 plugins/agent/native/src/owner/tests/selection.rs  |  16 +
 .../agent/native/src/owner/tests/service_tools.rs  | 453 ++++++++++++++++++---
 plugins/agent/native/src/tools.rs                  |  70 +++-
 plugins/agent/tools/src/cell.rs                    |   6 +-
 .../workspace/workspace-files/native/src/lib.rs    |  10 +
 products/maka/cli/notices.json                     |   2 +-
 products/maka/cli/src/application.rs               |  42 +-
 products/maka/cli/src/code.rs                      |   1 +
 products/maka/cli/tests/integration/application.rs |  14 +-
 24 files changed, 639 insertions(+), 110 deletions(-)

diff --git a/platform/event-log/tests/auxiliary_usage.rs b/platform/event-log/tests/auxiliary_usage.rs
index bef57de173..e4b5c416de 100644
--- a/platform/event-log/tests/auxiliary_usage.rs
+++ b/platform/event-log/tests/auxiliary_usage.rs
@@ -227,6 +227,7 @@ async fn title_accounting_uses_first_opening_after_stop_and_never_replays_after_
             connection_slug: "frozen".into(),
             model: "title-model".into(),
         }),
+        editing_tools: None,
         tool_discovery: false,
         tool_composition: None,
         system_prompt: None,
diff --git a/platform/event-log/tests/continuation/fixtures.rs b/platform/event-log/tests/continuation/fixtures.rs
index f2871b9946..1a766638e9 100644
--- a/platform/event-log/tests/continuation/fixtures.rs
+++ b/platform/event-log/tests/continuation/fixtures.rs
@@ -33,6 +33,7 @@ pub(super) fn opening(id: &str, claim: Option<ContinuationClaim>) -> RuntimeEven
                 approval_policy: maka_agent_facts::execution::ApprovalPolicy::OnRequest,
                 boundary_revision: 0,
                 system_prompt: None,
+                editing_tools: None,
                 tool_discovery: false,
                 tool_composition: None,
                 cwd: ".".into(),
diff --git a/platform/event-log/tests/executor.rs b/platform/event-log/tests/executor.rs
index 8fbdc53213..144ec3c491 100644
--- a/platform/event-log/tests/executor.rs
+++ b/platform/event-log/tests/executor.rs
@@ -62,6 +62,7 @@ async fn external_output_is_durable_observation_not_native_dispatch_and_rebuilds
             cwd: temp.path().to_string_lossy().into_owned(),
             workspace_identity: None,
             model: None,
+            editing_tools: None,
             tool_discovery: false,
             tool_composition: None,
             system_prompt: None,
diff --git a/plugins/agent/engine/tests/integration/support/invocation.rs b/plugins/agent/engine/tests/integration/support/invocation.rs
index 2d9b96fb06..a32a78641a 100644
--- a/plugins/agent/engine/tests/integration/support/invocation.rs
+++ b/plugins/agent/engine/tests/integration/support/invocation.rs
@@ -30,6 +30,7 @@ pub fn configuration(tool_mode: ToolMode) -> InvocationConfiguration {
         boundary_revision: 0,
         workspace_identity: None,
         system_prompt: None,
+        editing_tools: None,
         tool_discovery: false,
         tool_composition: None,
         cwd: std::env::current_dir()
diff --git a/plugins/agent/facts/src/execution.rs b/plugins/agent/facts/src/execution.rs
index be2d08541b..8d43525c35 100644
--- a/plugins/agent/facts/src/execution.rs
+++ b/plugins/agent/facts/src/execution.rs
@@ -108,6 +108,9 @@ impl SystemPrompt {
 /// Later Session mutations must not reinterpret relative paths or grants.
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct InvocationConfiguration {
+    /// Frozen file-editing policy. Older invocations retain their original unfiltered tools.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub editing_tools: Option<EditingTools>,
     /// Frozen discovery policy for this invocation, including continuations.
     #[serde(default, skip_serializing_if = "std::ops::Not::not")]
     pub tool_discovery: bool,
diff --git a/plugins/agent/facts/src/tool_output.rs b/plugins/agent/facts/src/tool_output.rs
index f0d0818305..1ecc0085f4 100644
--- a/plugins/agent/facts/src/tool_output.rs
+++ b/plugins/agent/facts/src/tool_output.rs
@@ -93,7 +93,22 @@ impl From<Value> for ToolOutput {
 }
 
 impl ToolOutput {
-    /// JavaScript and presentation see the original result, not model clipping.
+    /// Code Mode receives the full business result declared by the tool schema.
+    /// The journal retains the original operation receipt before this conversion.
+    pub fn into_code_value(self) -> Result<Value, &'static str> {
+        if let Self::Operation(reply) = self {
+            return match reply.receipt {
+                maka_plugin_core::operation::Receipt::Recorded {
+                    outcome: maka_plugin_core::operation::OperationOutcome::Returned { value },
+                    cleanup: maka_plugin_core::operation::OperationCleanup::Confirmed,
+                } => Ok(value),
+                _ => Err("Code Mode requires a confirmed returned operation receipt"),
+            };
+        }
+        Ok(self.into_json())
+    }
+
+    /// Original result evidence, without business projection or model clipping.
     pub fn into_json(self) -> Value {
         match self {
             Self::Model(result) => serde_json::to_value(result).expect("typed model result"),
diff --git a/plugins/agent/native/Cargo.toml b/plugins/agent/native/Cargo.toml
index 835ff3021d..63a8bdb366 100644
--- a/plugins/agent/native/Cargo.toml
+++ b/plugins/agent/native/Cargo.toml
@@ -27,6 +27,7 @@ maka-agent-facts.workspace = true
 maka-agent-ports.workspace = true
 maka-capabilities.workspace = true
 maka-files-native.workspace = true
+maka-workspace-api.workspace = true
 maka-workspace-support.workspace = true
 maka-skills-api.workspace = true
 maka-agent-authority.workspace = true
@@ -66,7 +67,6 @@ uuid.workspace = true
 
 [dev-dependencies]
 maka-external-agent.workspace = true
-maka-workspace-api.workspace = true
 maka-interactions-api.workspace = true
 maka-packages.workspace = true
 sqlx.workspace = true
diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md
index 3f4839e4d2..55ec4bf37b 100644
--- a/plugins/agent/native/README.md
+++ b/plugins/agent/native/README.md
@@ -25,8 +25,10 @@ Exposes Agent execution as native application services. `provide` registers fact
 
 The service owns its activation and delegates runs to `maka-agent-engine`. Session history, controls and tool calls use admitted scopes and original operation receipts. Files, processes, skills and other services are explicit dependencies selected by composition.
 
-The Agent captures the model's Code Mode default or explicit `codeMode` override when it accepts a message. Queued work and resumed runs retain their original mode. A service tool can opt into Code Mode with `nestable: true`; nested calls use the same permissions, operation receipts and cleanup as direct calls. Omitted `nestable` keeps the tool callable only directly. The Maka default composition enables nesting for files, processes, Web and Skills.
+The Agent captures the model's Code Mode default or explicit `codeMode` override when it accepts a message. Queued work and resumed runs retain their original mode. A service tool can opt into Code Mode with `nestable: true`; nested calls use the same permissions, operation receipts and cleanup as direct calls. JavaScript receives the declared business result; the durable journal retains the original native receipt. Omitted `nestable` keeps the tool callable only directly. The Maka defau [...]
 
 The default Maka composition enables `toolDiscovery`: optional tool schemas load through `tool_search`, with loaded schemas retained across turns in the same active Host session. Code Mode can still call authorized nested tools before their descriptions are loaded. Discovery policy is captured when a message is accepted and retained on continuation. Loaded schemas and Code Mode `store`/`load` values are session-local, in-memory state; restarting the Host clears them. Committed context co [...]
 
+The independent `applyPatch` model override selects either patch editing or structured Write/Edit tools. Like Code Mode, it defaults on for GPT and DeepSeek models and the DeepSeek endpoint, and is frozen for queued work and continuations. Selection follows the bound Files contract and method even when a composition renames its tools. Read tools remain available, and all file operations retain their live permission checks. The connection settings model-overrides JSON accepts `codeMode` a [...]
+
 [Source](src/lib.rs) · [Cargo manifest](Cargo.toml)
diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md
index c55f7b98bd..b5018abbbb 100644
--- a/plugins/agent/native/README.zh-CN.md
+++ b/plugins/agent/native/README.zh-CN.md
@@ -25,8 +25,10 @@
 
 服务管理自己的激活,并委托 `maka-agent-engine` 执行运行。会话历史、控制和工具调用使用获准作用域与原始操作回执。文件、进程、技能等服务都是通过组合显式选择的依赖。
 
-Agent 接收消息时保存模型的 Code Mode 默认值或显式 `codeMode` 覆盖设置;排队消息和恢复运行保留原模式。服务工具可以通过 `nestable: true` 允许代码内调用,仍执行与直接调用相同的权限检查、操作回执和清理流程。省略该字段时仅允许直接调用。Maka 默认组合为文件、进程、Web 和 Skills 工具启用嵌套调用。
+Agent 接收消息时保存模型的 Code Mode 默认值或显式 `codeMode` 覆盖设置;排队消息和恢复运行保留原模式。服务工具可以通过 `nestable: true` 允许代码内调用,仍执行与直接调用相同的权限检查、操作回执和清理流程。JavaScript 收到声明的业务结果,持久日志保留原生操作的原始回执。省略该字段时仅允许直接调用。Maka 默认组合为文件、进程、Web 和 Skills 工具启用嵌套调用。
 
 Maka 默认组合启用 `toolDiscovery`:可选工具通过 `tool_search` 加载说明,并在同一 Host 会话内跨轮保留。Code Mode 在说明尚未加载时仍可调用已授权的嵌套工具。消息接收时保存发现策略,恢复运行时沿用原策略。已加载工具说明和 Code Mode 的 `store`/`load` 值按会话隔离、保存在内存中,Host 重启后清空;上下文压缩成功提交后重置已加载的工具说明。
 
+独立的模型覆盖项 `applyPatch` 选择补丁编辑或 Write/Edit 结构化编辑。与 Code Mode 一样,GPT、DeepSeek 模型及 DeepSeek 端点默认启用;排队消息和恢复运行保留接收时的选择。筛选依据是实际绑定的 Files 契约和方法,工具改名不影响策略。读取工具保留,文件操作仍检查实时权限。连接设置的模型覆盖 JSON 可分别设置 `codeMode` 与 `applyPatch`。
+
 [源码](src/lib.rs) · [Cargo 配置](Cargo.toml)
diff --git a/plugins/agent/native/src/model.rs b/plugins/agent/native/src/model.rs
index 92d15ec204..36bbf97f03 100644
--- a/plugins/agent/native/src/model.rs
+++ b/plugins/agent/native/src/model.rs
@@ -25,6 +25,7 @@ use maka_agent_facts::composition::SourceRevision;
 use maka_agent_facts::configuration::ModelCapabilities;
 use maka_agent_facts::configuration::ModelInfo;
 use maka_agent_facts::configuration::ModelSelection;
+use maka_agent_facts::execution::EditingTools;
 use maka_agent_facts::execution::ModelBinding;
 use maka_agent_facts::execution::ThinkingLevel;
 use maka_agent_facts::execution::ToolMode;
@@ -74,6 +75,9 @@ pub(crate) struct AcceptedModel {
     /// do not reinterpret queued work. Older accepted messages remain Direct.
     #[serde(default)]
     pub tool_mode: ToolMode,
+    /// None preserves the unfiltered tool selection of older accepted messages.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub editing_tools: Option<EditingTools>,
     provider: maka_agent_facts::provider::Identity,
     provider_configuration: Value,
     basis: ExecutionBasis,
@@ -114,6 +118,14 @@ impl Source {
             configuration,
             credentials,
             accepted: AcceptedModel {
+                editing_tools: Some(EditingTools::for_model(
+                    &selection.model_id,
+                    &model.base_url,
+                    row.model_overrides
+                        .as_ref()
+                        .and_then(|models| models.get(&selection.model_id))
+                        .and_then(|overrides| overrides.apply_patch),
+                )),
                 tool_mode: ToolMode::for_model(
                     &selection.model_id,
                     &model.base_url,
diff --git a/plugins/agent/native/src/owner.rs b/plugins/agent/native/src/owner.rs
index 62757f5d19..f52c9ae125 100644
--- a/plugins/agent/native/src/owner.rs
+++ b/plugins/agent/native/src/owner.rs
@@ -130,6 +130,7 @@ pub(crate) struct Owner {
     engine: Engine,
     tools: maka_tools::ToolCatalog,
     tool_basis: Vec<crate::tools::SelectedTool>,
+    editing_tools: Vec<(String, maka_agent_facts::execution::EditingTools)>,
     skills: Option<skills::Library>,
     executors: crate::executor::Executors,
     runs: Arc<Mutex<Runs>>,
@@ -260,22 +261,25 @@ impl Owner {
             additional,
             workers.clone(),
         )
-        .and_then(|(tools, basis)| {
-            let tools = if settings.recall {
-                tools
+        .and_then(|mut catalog| {
+            if settings.recall {
+                catalog.tools = catalog
+                    .tools
                     .with_request_binding(
                         maka_agent_facts::tools::ToolPreparer::names(recall.as_ref())
                             .into_iter()
                             .collect(),
                         recall.clone(),
                     )
-                    .map_err(|error| error.to_string())?
-            } else {
-                tools
-            };
-            Ok((tools, basis))
+                    .map_err(|error| error.to_string())?;
+            }
+            Ok(catalog)
         });
-        let (tools, tool_basis) = match prepared_tools {
+        let crate::tools::Catalog {
+            tools,
+            basis: tool_basis,
+            editing: editing_tools,
+        } = match prepared_tools {
             Ok(tools) => tools,
             Err(error) => {
                 configuration
@@ -321,6 +325,7 @@ impl Owner {
             engine,
             tools,
             tool_basis,
+            editing_tools,
             skills,
             executors,
             runs: Arc::default(),
@@ -605,6 +610,7 @@ impl Owner {
         &self,
         allowed: Option<&[String]>,
         discovery: bool,
+        editing: Option<maka_agent_facts::execution::EditingTools>,
     ) -> maka_tools::ToolCatalog {
         let selected = match allowed {
             None => self.tools.clone(),
@@ -618,6 +624,18 @@ impl Owner {
                 self.tools.excluding(&excluded)
             }
         };
+        let selected = match editing {
+            None => selected,
+            Some(editing) => {
+                let excluded: Vec<_> = self
+                    .editing_tools
+                    .iter()
+                    .filter(|(_, style)| *style != editing)
+                    .map(|(name, _)| name.as_str())
+                    .collect();
+                selected.excluding(&excluded)
+            }
+        };
         if discovery {
             selected.with_discovery()
         } else {
@@ -784,8 +802,12 @@ impl Owner {
                 if basis.executor.is_some() {
                     vec![]
                 } else {
-                    self.selected_tools(session.configuration.tools.as_deref(), false)
-                        .names()
+                    self.selected_tools(
+                        session.configuration.tools.as_deref(),
+                        false,
+                        basis.model.as_ref().and_then(|model| model.editing_tools),
+                    )
+                    .names()
                 },
             )
             .await?
diff --git a/plugins/agent/native/src/owner/execution.rs b/plugins/agent/native/src/owner/execution.rs
index b17dbe6c1d..771941ff41 100644
--- a/plugins/agent/native/src/owner/execution.rs
+++ b/plugins/agent/native/src/owner/execution.rs
@@ -287,6 +287,7 @@ impl Owner {
         // A continuation retains the original tool protocol even when the
         // user selects a model whose default differs from the original Run.
         source.accepted.tool_mode = configuration.tool_mode;
+        source.accepted.editing_tools = configuration.editing_tools;
         let basis = MessageBasis {
             tool_discovery: configuration.tool_discovery,
             allowed_tools: session.configuration.tools.clone(),
@@ -313,6 +314,7 @@ impl Owner {
         let tools = self.selected_tools(
             session.configuration.tools.as_deref(),
             configuration.tool_discovery,
+            configuration.editing_tools,
         );
         let run = RunInput {
             invocation: invocation.clone(),
diff --git a/plugins/agent/native/src/owner/queue.rs b/plugins/agent/native/src/owner/queue.rs
index 7810c2781f..067239de41 100644
--- a/plugins/agent/native/src/owner/queue.rs
+++ b/plugins/agent/native/src/owner/queue.rs
@@ -304,6 +304,7 @@ impl Owner {
             prompt.validate().map_err(|_| "agent_prompt_invalid")?;
         }
         let mut configuration = InvocationConfiguration {
+            editing_tools: None,
             tool_discovery: basis.tool_discovery,
             cwd: basis.workspace.path,
             workspace_identity: Some(basis.workspace.identity),
@@ -325,6 +326,12 @@ impl Owner {
                 configuration.model = Some(source.accepted.binding.clone());
                 configuration.thinking_level = source.accepted.thinking_level;
                 configuration.tool_mode = source.accepted.tool_mode;
+                configuration.editing_tools = source.accepted.editing_tools;
+                let tools = self.selected_tools(
+                    basis.allowed_tools.as_deref(),
+                    basis.tool_discovery,
+                    configuration.editing_tools,
+                );
                 PreparedRun::Model(Box::new(RunInput {
                     model_source: Some(Arc::new(source)),
                     model_revision: Some(prepared.revision),
@@ -340,8 +347,7 @@ impl Owner {
                     work: RunWork::Message {
                         message: delivered.message.content.clone(),
                         source_messages: vec![delivered],
-                        tools: self
-                            .selected_tools(basis.allowed_tools.as_deref(), basis.tool_discovery),
+                        tools,
                         max_steps: basis.max_steps,
                         allow_prior_unknown: false,
                     },
diff --git a/plugins/agent/native/src/owner/tests.rs b/plugins/agent/native/src/owner/tests.rs
index c27cb44228..269f3b9fed 100644
--- a/plugins/agent/native/src/owner/tests.rs
+++ b/plugins/agent/native/src/owner/tests.rs
@@ -540,20 +540,23 @@ impl Fixture {
             tools: vec![],
         };
         configure(&mut settings);
+        let selected_max_steps = settings.max_steps;
         if settings.recall || settings.scheduled_tasks {
             settings.max_steps = 6;
         }
         if let Some(tooling) = &tooling {
             settings.tools = tooling.selections();
-            settings.max_steps = if settings
-                .tools
-                .iter()
-                .any(|tool| tool.input == contract::ToolInput::Skill)
-            {
-                8
-            } else {
-                2
-            };
+            settings.max_steps = selected_max_steps.max(
+                if settings
+                    .tools
+                    .iter()
+                    .any(|tool| tool.input == contract::ToolInput::Skill)
+                {
+                    8
+                } else {
+                    2
+                },
+            );
         }
         let (mut input, mut packages) = input(&settings);
         if let Some(tooling) = &tooling {
diff --git a/plugins/agent/native/src/owner/tests/files.rs b/plugins/agent/native/src/owner/tests/files.rs
index d2d83b39ad..9afbc2d4c9 100644
--- a/plugins/agent/native/src/owner/tests/files.rs
+++ b/plugins/agent/native/src/owner/tests/files.rs
@@ -32,6 +32,18 @@ async fn fixture(method: &'static str) -> (TcpListener, Fixture, String) {
         Some(service_tools::Setup::files(method)),
     )
     .await;
+    if method == "applyPatch" {
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([(
+                "fixture-model".into(),
+                ModelOverride {
+                    apply_patch: Some(true),
+                    ..Default::default()
+                },
+            )]));
+        })
+        .await;
+    }
     let session = fixture.create("Native workspace files").await;
     (listener, fixture, session)
 }
diff --git a/plugins/agent/native/src/owner/tests/selection.rs b/plugins/agent/native/src/owner/tests/selection.rs
index c238f4a678..94b3c65086 100644
--- a/plugins/agent/native/src/owner/tests/selection.rs
+++ b/plugins/agent/native/src/owner/tests/selection.rs
@@ -718,6 +718,7 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
             "fixture-model".into(),
             ModelOverride {
                 code_mode: Some(true),
+                apply_patch: Some(false),
                 ..Default::default()
             },
         )]));
@@ -734,12 +735,17 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
     .await
     .unwrap();
     assert_eq!(source.accepted.tool_mode, ToolMode::CodeMode);
+    assert_eq!(
+        source.accepted.editing_tools,
+        Some(maka_agent_facts::execution::EditingTools::Structured)
+    );
     let stored = serde_json::to_value(&source.accepted).unwrap();
     edit(&fixture, |changes| {
         changes.model_overrides = Patch::Set(BTreeMap::from([(
             "fixture-model".into(),
             ModelOverride {
                 code_mode: Some(false),
+                apply_patch: Some(true),
                 ..Default::default()
             },
         )]));
@@ -755,6 +761,10 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
     .unwrap();
     restored.capture().await.unwrap();
     assert_eq!(restored.accepted.tool_mode, ToolMode::CodeMode);
+    assert_eq!(
+        restored.accepted.editing_tools,
+        Some(maka_agent_facts::execution::EditingTools::Structured)
+    );
     let (next, _) = crate::model::Source::select(
         fixture.config.clone(),
         fixture.owner.credentials.clone(),
@@ -766,10 +776,16 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
     .await
     .unwrap();
     assert_eq!(next.accepted.tool_mode, ToolMode::Direct);
+    assert_eq!(
+        next.accepted.editing_tools,
+        Some(maka_agent_facts::execution::EditingTools::ApplyPatch)
+    );
     let mut legacy = stored;
     legacy.as_object_mut().unwrap().remove("tool_mode");
+    legacy.as_object_mut().unwrap().remove("editing_tools");
     let accepted: crate::model::AcceptedModel = serde_json::from_value(legacy).unwrap();
     assert_eq!(accepted.tool_mode, ToolMode::Direct);
+    assert_eq!(accepted.editing_tools, None);
     fixture.app.shutdown().await.unwrap();
     fixture.config.shutdown().await.unwrap();
 }
diff --git a/plugins/agent/native/src/owner/tests/service_tools.rs b/plugins/agent/native/src/owner/tests/service_tools.rs
index c2a11d3b2e..6160696fa8 100644
--- a/plugins/agent/native/src/owner/tests/service_tools.rs
+++ b/plugins/agent/native/src/owner/tests/service_tools.rs
@@ -138,6 +138,7 @@ pub(super) struct Setup {
     questions: bool,
     forms: bool,
     files: Option<&'static str>,
+    all_file_editors: bool,
     processes: bool,
     isolated_workspaces: bool,
     web: bool,
@@ -166,6 +167,7 @@ impl Setup {
             questions: false,
             forms: false,
             files: None,
+            all_file_editors: false,
             processes: false,
             isolated_workspaces: false,
             web: false,
@@ -191,6 +193,7 @@ impl Setup {
             questions: false,
             forms: false,
             files: None,
+            all_file_editors: false,
             processes: false,
             isolated_workspaces: false,
             web: false,
@@ -222,6 +225,7 @@ impl Setup {
             questions: true,
             forms: false,
             files: None,
+            all_file_editors: false,
             processes: false,
             isolated_workspaces: false,
             web: false,
@@ -313,6 +317,22 @@ impl Setup {
         if self.protocol || (self.skills.is_some() && !self.skill_tools) {
             return vec![];
         }
+        if self.all_file_editors {
+            return [
+                ("readText", "InspectDocument"),
+                ("write", "ReplaceDocument"),
+                ("edit", "AdjustDocument"),
+                ("applyPatch", "PatchDocument"),
+            ]
+            .into_iter()
+            .map(|(method, name)| contract::ServiceTool {
+                method: method.into(),
+                name: name.into(),
+                presentation: None,
+                ..self.selection()
+            })
+            .collect();
+        }
         if self.web {
             return [("fetch", "WebFetch"), ("search", "WebSearch")]
                 .into_iter()
@@ -1283,56 +1303,76 @@ async fn unlisted_native_tool_edge_never_executes_the_selected_provider() {
 }
 #[tokio::test]
 async fn stopping_real_agent_run_cancels_tool_but_retains_capacity_until_physical_cleanup() {
-    tokio::time::timeout(Duration::from_secs(30), async {
-        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
-        let base = format!("http://{}/v1", listener.local_addr().unwrap());
-        let server = tokio::spawn(async move {
-            let (mut socket, _) = listener.accept().await.unwrap();
-            request(&mut socket).await;
-            tool_response(&mut socket).await;
-        });
-        let counter = Counter::new(Behavior::Blocked);
-        let fixture = Fixture::with_services(
-            &base,
-            1,
-            None,
-            None,
-            Some(Setup::native(counter.clone(), true)),
-        )
-        .await;
-        let session = fixture.create("Stop original tool").await;
-        let started = fixture.send(&session).await.unwrap();
-        counter.entered.acquire().await.unwrap().forget();
-        fixture
-            .mutation(
-                2,
-                "stop",
-                json!({"sessionId":session,"invocationId":started["invocationId"]}),
-                Uuid::new_v4(),
+    for code in [false, true] {
+        tokio::time::timeout(Duration::from_secs(30), async {
+            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let base = format!("http://{}/v1", listener.local_addr().unwrap());
+            let server = tokio::spawn(async move {
+                let (mut socket, _) = listener.accept().await.unwrap();
+                request(&mut socket).await;
+                if code {
+                    code_response(&mut socket, "text(await tools.Counter({amount:1}));").await;
+                } else {
+                    tool_response(&mut socket).await;
+                }
+            });
+            let counter = Counter::new(Behavior::Blocked);
+            let fixture = Fixture::with_services(
+                &base,
+                1,
+                None,
+                None,
+                Some({
+                    let mut setup = Setup::native(counter.clone(), true);
+                    setup.nestable = code;
+                    setup
+                }),
             )
-            .await
-            .unwrap();
-        counter.cancelled.acquire().await.unwrap().forget();
-        assert_eq!(fixture.owner.permits.available_permits(), 0);
-        assert_eq!(fixture.owner.workers.len(), 1);
-        let other = fixture.create("Not available yet").await;
-        assert!(
+            .await;
+            selection::edit(&fixture, |changes| {
+                changes.model_overrides = Patch::Set(BTreeMap::from([(
+                    "fixture-model".into(),
+                    ModelOverride {
+                        code_mode: Some(code),
+                        ..Default::default()
+                    },
+                )]));
+            })
+            .await;
+            let session = fixture.create("Stop original tool").await;
+            let started = fixture.send(&session).await.unwrap();
+            counter.entered.acquire().await.unwrap().forget();
             fixture
-                .send(&other)
+                .mutation(
+                    2,
+                    "stop",
+                    json!({"sessionId":session,"invocationId":started["invocationId"]}),
+                    Uuid::new_v4(),
+                )
                 .await
-                .unwrap_err()
-                .contains("agent_capacity_exhausted")
-        );
-        counter.release.add_permits(1);
-        fixture.drained().await;
-        server.await.unwrap();
-        assert_eq!(counter.effects.load(Ordering::SeqCst), 1);
-        assert_eq!(fixture.owner.permits.available_permits(), 1);
-        fixture.app.shutdown().await.unwrap();
-        fixture.config.shutdown().await.unwrap();
-    })
-    .await
-    .unwrap();
+                .unwrap();
+            counter.cancelled.acquire().await.unwrap().forget();
+            assert_eq!(fixture.owner.permits.available_permits(), 0);
+            assert_eq!(fixture.owner.workers.len(), 1);
+            let other = fixture.create("Not available yet").await;
+            assert!(
+                fixture
+                    .send(&other)
+                    .await
+                    .unwrap_err()
+                    .contains("agent_capacity_exhausted")
+            );
+            counter.release.add_permits(1);
+            fixture.drained().await;
+            server.await.unwrap();
+            assert_eq!(counter.effects.load(Ordering::SeqCst), 1);
+            assert_eq!(fixture.owner.permits.available_permits(), 1);
+            fixture.app.shutdown().await.unwrap();
+            fixture.config.shutdown().await.unwrap();
+        })
+        .await
+        .unwrap();
+    }
 }
 #[tokio::test]
 async fn returned_effect_with_failed_cleanup_preserves_receipt_without_settling_or_reusing_capacity()
@@ -1734,7 +1774,7 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
             ).await;
             selection::edit(&fixture, |changes| {
                 changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
-                    code_mode: Some(true), ..Default::default()
+                    code_mode: Some(true), apply_patch: Some(true), ..Default::default()
                 })]));
             }).await;
             let session = fixture.create("Resume settled tool").await;
@@ -1751,7 +1791,7 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
             assert_eq!(source["execution"]["state"], "cancelled");
             selection::edit(&fixture, |changes| {
                 changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
-                    code_mode: Some(false), ..Default::default()
+                    code_mode: Some(false), apply_patch: Some(false), ..Default::default()
                 })]));
             }).await;
             let resumed = fixture.mutation(2, "resume", json!({
@@ -1771,6 +1811,10 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
             response(&mut replay).await;
             fixture.drained().await;
             let observed = facts(&fixture, &session).await;
+            assert!(observed.iter().filter_map(|fact| match fact {
+                Fact::InvocationOpened { configuration: Some(configuration), .. } => Some(configuration),
+                _ => None,
+            }).all(|configuration| configuration.editing_tools == Some(maka_agent_facts::execution::EditingTools::ApplyPatch)), "resume retains original editing policy after overrides change");
             assert_eq!(observed.iter().filter(|fact| matches!(fact, Fact::ToolDispatched { .. })).count(), 1);
             if matches!(behavior, Behavior::Unknown) {
                 let reference = observed.iter().find_map(|fact| match fact {
@@ -1843,7 +1887,7 @@ async fn code_mode_calls_selected_native_services_with_original_grants_and_recei
                 let names: Vec<_> = first["tools"].as_array().unwrap().iter().filter_map(|tool| tool["function"]["name"].as_str()).collect();
                 assert!(names.contains(&"exec") && names.contains(&"wait"));
                 assert_eq!(names.contains(&"Counter"), !nestable);
-                let code = "text(await tools.Counter({amount:1})); text(await tools.Counter({amount:1}));";
+                let code = "const first = await tools.Counter({amount:1}); if (first.effects !== 1 || first.receipt !== undefined) throw Error('native result differs from declared schema'); text(first); const second = await tools.Counter({amount:1}); if (second.effects !== 2) throw Error('second effect missing'); text(second);";
                 code_response(&mut socket, code).await;
                 let (mut socket, _) = listener.accept().await.unwrap();
                 let next = request(&mut socket).await;
@@ -1873,6 +1917,16 @@ async fn code_mode_calls_selected_native_services_with_original_grants_and_recei
                         Fact::ToolSettled { operation_id, outcome: ToolOutcome::Succeeded { .. } }
                         if operation_id == expected
                     )).count(), 1, "each nested service has its own successful durable settlement");
+                    let prefix = fixture.store.scoped_prefix(
+                        maka_agent_facts::event::LogScope::Session { id: session.clone() },
+                        256, 4 * 1024 * 1024,
+                    ).await.unwrap();
+                    let event = prefix.events.iter().find(|event| matches!(&event.event.fact,
+                        Fact::ToolSettled { operation_id, .. } if operation_id == expected
+                    )).unwrap();
+                    assert!(matches!(fixture.store.resolve_tool_result(&session, &event.event.id).await.unwrap(),
+                        maka_agent_facts::tool_output::ToolOutput::Operation(_)
+                    ), "Code Mode business projection must retain the original native receipt");
                 }
             }
             fixture.app.shutdown().await.unwrap();
@@ -2046,3 +2100,302 @@ async fn code_mode_discovery_preserves_session_store_and_nested_callability_betw
         fixture.config.shutdown().await.unwrap();
     }).await.unwrap();
 }
+
+fn file_editors(nestable: bool) -> Setup {
+    let mut setup = Setup::files("readText");
+    setup.all_file_editors = true;
+    setup.nestable = nestable;
+    setup
+}
+
+fn create_document(patch: bool, path: &str) -> (&'static str, Value) {
+    if patch {
+        (
+            "PatchDocument",
+            json!({"callId":"policy-fixture","operation":{"type":"create_file","path":path,"diff":"+editing policy\n"}}),
+        )
+    } else {
+        (
+            "ReplaceDocument",
+            json!({"path":path,"content":"editing policy"}),
+        )
+    }
+}
+
+#[tokio::test]
+async fn model_editing_policy_selects_bound_file_methods_independently_of_code_mode() {
+    tokio::time::timeout(Duration::from_secs(90), async {
+        for (model, code, patch, writable) in [
+            ("fixture-model", None, None, true),
+            ("gpt-fixture", None, None, true),
+            ("fixture-model", Some(false), Some(false), true),
+            ("fixture-model", Some(false), Some(true), true),
+            ("fixture-model", Some(true), Some(false), true),
+            ("fixture-model", Some(true), Some(true), true),
+            ("fixture-model", Some(true), Some(true), false),
+        ] {
+            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let expected_code = code.unwrap_or(model == "gpt-fixture");
+            let expected_patch = patch.unwrap_or(model == "gpt-fixture");
+            let fixture = Fixture::configured(
+                &format!("http://{}/v1", listener.local_addr().unwrap()), 1, None, None,
+                Some(file_editors(true)), |settings| settings.tool_discovery = expected_code,
+            ).await;
+            selection::edit(&fixture, |changes| {
+                changes.enabled_model_ids = Some(vec![model.into()]);
+                changes.model_overrides = Patch::Set(BTreeMap::from([(model.into(), ModelOverride {
+                    code_mode: code, apply_patch: patch, ..Default::default()
+                })]));
+            }).await;
+            let session = fixture.create("Independent file editing policy").await;
+            if !writable {
+                files::change(&fixture, &session, "read_only", false, true).await;
+            }
+            let mut selection = fixture.selection().await;
+            selection.model_id = model.into();
+            fixture.mutation(2, "send", json!({"sessionId":session,"message":"Use the selected editor","model":selection}), Uuid::new_v4()).await.unwrap();
+            let (mut socket, _) = listener.accept().await.unwrap();
+            let prompt = request(&mut socket).await;
+            assert_eq!(has_tool(&prompt, "exec"), expected_code);
+            let (name, input) = create_document(expected_patch, "policy.txt");
+            if expected_code {
+                let excluded = if expected_patch { "ReplaceDocument" } else { "PatchDocument" };
+                let code = format!("if (ALL_TOOLS.some(t => t.name === '{excluded}')) throw Error('excluded editor leaked'); if (!ALL_TOOLS.some(t => t.name === 'InspectDocument')) throw Error('reader lost'); text(await tools.{name}({input}));");
+                code_response(&mut socket, &code).await;
+            } else {
+                assert!(has_tool(&prompt, "InspectDocument"));
+                assert_eq!(has_tool(&prompt, "ReplaceDocument"), !expected_patch);
+                assert_eq!(has_tool(&prompt, "AdjustDocument"), !expected_patch);
+                assert_eq!(has_tool(&prompt, "PatchDocument"), expected_patch);
+                named_tool_response(&mut socket, name, input).await;
+            }
+            let (mut socket, _) = listener.accept().await.unwrap();
+            request(&mut socket).await;
+            let target = fixture.workspace.join("policy.txt");
+            assert_eq!(target.exists(), writable);
+            if writable { assert_eq!(std::fs::read_to_string(target).unwrap(), "editing policy"); }
+            response(&mut socket).await;
+            fixture.drained().await;
+            let expected = if expected_patch { maka_agent_facts::execution::EditingTools::ApplyPatch } else { maka_agent_facts::execution::EditingTools::Structured };
+            assert!(facts(&fixture, &session).await.iter().any(|fact| matches!(fact,
+                Fact::InvocationOpened { configuration: Some(configuration), .. } if configuration.editing_tools == Some(expected)
+            )));
+            fixture.app.shutdown().await.unwrap();
+            fixture.config.shutdown().await.unwrap();
+        }
+    }).await.unwrap();
+}
+
+#[tokio::test]
+async fn queued_file_editing_policy_is_frozen_and_new_messages_adopt_changed_overrides() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let fixture = Fixture::with_services(
+            &format!("http://{}/v1", listener.local_addr().unwrap()), 1, None, None,
+            Some(file_editors(true)),
+        ).await;
+        let session = fixture.create("Queued editing policy").await;
+        fixture.send(&session).await.unwrap();
+        let (mut first, _) = listener.accept().await.unwrap();
+        let original = request(&mut first).await;
+        assert!(has_tool(&original, "ReplaceDocument"));
+        fixture.send(&session).await.unwrap();
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
+                code_mode: Some(true), apply_patch: Some(true), ..Default::default()
+            })]));
+        }).await;
+        response(&mut first).await;
+        let (mut queued, _) = listener.accept().await.unwrap();
+        let prompt = request(&mut queued).await;
+        assert_eq!(prompt["tools"], original["tools"], "queued work keeps the admitted editor and tool protocol");
+        let (name, input) = create_document(false, "queued-policy.txt");
+        named_tool_response(&mut queued, name, input).await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        response(&mut socket).await;
+        fixture.drained().await;
+        assert_eq!(std::fs::read_to_string(fixture.workspace.join("queued-policy.txt")).unwrap(), "editing policy");
+
+        fixture.send(&session).await.unwrap();
+        let (mut socket, _) = listener.accept().await.unwrap();
+        let prompt = request(&mut socket).await;
+        assert!(has_tool(&prompt, "exec"));
+        let (name, input) = create_document(true, "new-policy.txt");
+        code_response(&mut socket, &format!("if (ALL_TOOLS.some(t => t.name === 'ReplaceDocument')) throw Error('old editor leaked'); text(await tools.{name}({input}));")).await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        response(&mut socket).await;
+        fixture.drained().await;
+        assert_eq!(std::fs::read_to_string(fixture.workspace.join("new-policy.txt")).unwrap(), "editing policy");
+        fixture.app.shutdown().await.unwrap();
+        fixture.config.shutdown().await.unwrap();
+    }).await.unwrap();
+}
+
+#[cfg(target_os = "macos")]
+#[tokio::test]
+async fn code_mode_wait_observes_original_native_process_and_its_workspace_effect() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let mut setup = Setup::processes();
+        setup.nestable = true;
+        let fixture = Fixture::configured(
+            &format!("http://{}/v1", listener.local_addr().unwrap()), 1, None, None,
+            Some(setup), |settings| { settings.tool_discovery = true; settings.max_steps = 3; },
+        ).await;
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
+                code_mode: Some(true), ..Default::default()
+            })]));
+        }).await;
+        let session = fixture.create("Wait for original native process").await;
+        fixture.send(&session).await.unwrap();
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        code_response(&mut socket, "// @exec: {\"yield_time_ms\":0}\nconst result = await tools.Execute({command:'printf started > process-started; while [ ! -f process-release ]; do sleep 0.02; done; printf native-code-mode > process-result; cat process-result',login:false,timeout_ms:30000}); if (result.kind !== 'completed' || result.value.status !== 'completed') throw Error(JSON.stringify(result)); text(result.value.output.stdout);").await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        let prompt = request(&mut socket).await;
+        let message = prompt["messages"].as_array().unwrap().iter().rev().find(|message| message["role"] == "tool").unwrap();
+        let running: Value = serde_json::from_str(message["content"].as_str().unwrap()).unwrap();
+        assert_eq!(running["state"], "running", "{running}");
+        let cell = running["cell_id"].as_str().unwrap();
+        tokio::time::timeout(Duration::from_secs(20), async {
+            while !fixture.workspace.join("process-started").exists() {
+                tokio::time::sleep(Duration::from_millis(10)).await;
+            }
+        }).await.unwrap();
+        std::fs::write(fixture.workspace.join("process-release"), "release").unwrap();
+        named_tool_response(&mut socket, "wait", json!({"cell_id":cell,"yield_time_ms":30000})).await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        let completed = request(&mut socket).await;
+        let message = completed["messages"].as_array().unwrap().iter().rev().find(|message| message["role"] == "tool").unwrap();
+        let text = message["content"].as_str().unwrap();
+        let result: Value = serde_json::from_str(text.lines().next().unwrap()).unwrap();
+        assert_eq!(result["state"], "completed", "{result}");
+        assert_eq!(result["result"]["ok"], true, "{result}");
+        assert!(text.lines().skip(1).any(|line| line == "native-code-mode"), "{text}");
+        assert_eq!(std::fs::read_to_string(fixture.workspace.join("process-result")).unwrap(), "native-code-mode");
+        response(&mut socket).await;
+        fixture.drained().await;
+        let observed = facts(&fixture, &session).await;
+        assert_eq!(observed.iter().filter(|fact| matches!(fact, Fact::ToolDispatched {name, ..} if name == "Execute")).count(), 1);
+        assert_eq!(fixture.owner.permits.available_permits(), 1);
+        fixture.app.shutdown().await.unwrap();
+        fixture.config.shutdown().await.unwrap();
+    }).await.unwrap();
+}
+
+#[tokio::test]
+async fn code_mode_native_image_bytes_reach_the_vision_model() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let mut setup = Setup::files("readImage");
+        setup.nestable = true;
+        let fixture = Fixture::configured(
+            &format!("http://{}/v1", listener.local_addr().unwrap()),
+            1,
+            None,
+            None,
+            Some(setup),
+            |settings| settings.tool_discovery = true,
+        )
+        .await;
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([(
+                "fixture-model".into(),
+                ModelOverride {
+                    code_mode: Some(true),
+                    vision: Some(true),
+                    ..Default::default()
+                },
+            )]));
+        })
+        .await;
+        let session = fixture.create("Nested native image").await;
+        let bytes = images::gif(0);
+        std::fs::write(fixture.workspace.join("nested.gif"), &bytes).unwrap();
+        files::change(&fixture, &session, "read_only", false, false).await;
+        fixture.send(&session).await.unwrap();
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        code_response(
+            &mut socket,
+            "image(await tools.WorkspaceFile({path:'nested.gif'}), 'original');",
+        )
+        .await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        let prompt = request(&mut socket).await;
+        assert_eq!(images::model_images(&prompt), vec![bytes]);
+        response(&mut socket).await;
+        fixture.drained().await;
+        assert_eq!(fixture.owner.permits.available_permits(), 1);
+        fixture.app.shutdown().await.unwrap();
+        fixture.config.shutdown().await.unwrap();
+    })
+    .await
+    .unwrap();
+}
+
+#[tokio::test]
+async fn editing_policy_and_code_mode_survive_actual_host_restart_with_original_native_effect() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let setup = file_editors(true);
+        let policy = setup.policy();
+        let fixture = Fixture::with_services(
+            &format!("http://{}/v1", listener.local_addr().unwrap()), 1, None, None, Some(setup),
+        ).await;
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
+                code_mode: Some(true), apply_patch: Some(false), ..Default::default()
+            })]));
+        }).await;
+        let session = fixture.create("Restore admitted tool policies").await;
+        let deployment = fixture.app.client().snapshot().effective.unwrap().deployment;
+        let raw = rusqlite::Connection::open(fixture.root.join(maka_event_log::root::ROOT_DATABASE)).unwrap();
+        raw.execute_batch("CREATE TRIGGER hold_opening BEFORE INSERT ON event_log WHEN NEW.kind='invocation_opened' BEGIN SELECT RAISE(ABORT, 'fixture opening failure'); END;").unwrap();
+        assert!(fixture.send(&session).await.is_err());
+        assert_eq!(fixture.store.pending_messages(&session).await.unwrap().len(), 1);
+        selection::edit(&fixture, |changes| {
+            changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
+                code_mode: Some(false), apply_patch: Some(true), ..Default::default()
+            })]));
+        }).await;
+        fixture.app.shutdown().await.unwrap();
+        raw.execute_batch("DROP TRIGGER hold_opening").unwrap();
+        drop(raw);
+        let mut native = NativeRegistry::default();
+        provide_all(&mut native).unwrap();
+        maka_settings_native::provide(&mut native).unwrap();
+        let restored = Application::start_from_store(
+            fixture.store.clone(), deployment, native, fixture.issuer.clone(),
+            Arc::new(Grant(fixture.issuer.clone(), Some(policy))), Some(maka_host_js::factory(Default::default())),
+        ).await.unwrap();
+        restored.ready().await.unwrap();
+        let next = tokio::time::timeout(Duration::from_secs(10), listener.accept()).await;
+        assert!(next.is_ok(), "restored admission did not reach model: {:?}", facts(&fixture, &session).await);
+        let (mut socket, _) = next.unwrap().unwrap();
+        let prompt = request(&mut socket).await;
+        assert!(has_tool(&prompt, "exec"), "Host restart retains the admitted Code Mode");
+        let (name, input) = create_document(false, "restored-policy.txt");
+        code_response(&mut socket, &format!("if (ALL_TOOLS.some(t => t.name === 'PatchDocument')) throw Error('changed policy leaked'); text(await tools.{name}({input}));")).await;
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        assert_eq!(std::fs::read_to_string(fixture.workspace.join("restored-policy.txt")).unwrap(), "editing policy");
+        let mut commits = fixture.store.subscribe_commits();
+        response(&mut socket).await;
+        loop {
+            let recorded = facts(&fixture, &session).await;
+            if recorded.iter().any(|fact| matches!(fact, Fact::InvocationEnded { .. })) {
+                assert!(recorded.iter().any(|fact| matches!(fact, Fact::InvocationEnded { outcome: maka_agent_facts::event::InvocationOutcome::Completed })));
+                assert_eq!(recorded.iter().filter(|fact| matches!(fact, Fact::ToolDispatched { name, .. } if name == "ReplaceDocument")).count(), 1);
+                assert!(recorded.iter().any(|fact| matches!(fact, Fact::InvocationOpened { configuration: Some(configuration), .. } if configuration.editing_tools == Some(maka_agent_facts::execution::EditingTools::Structured))));
+                break;
+            }
+            commits.changed().await.unwrap();
+        }
+        restored.shutdown().await.unwrap();
+        fixture.config.shutdown().await.unwrap();
+    }).await.unwrap();
+}
diff --git a/plugins/agent/native/src/tools.rs b/plugins/agent/native/src/tools.rs
index 3da5f62b3a..20ae9d4d47 100644
--- a/plugins/agent/native/src/tools.rs
+++ b/plugins/agent/native/src/tools.rs
@@ -23,6 +23,7 @@ pub(crate) mod image;
 mod web;
 use crate::contract::{ServiceTool, ToolConfirmation, ToolInput, ToolResult};
 use futures_util::FutureExt;
+use maka_agent_facts::execution::EditingTools;
 use maka_agent_facts::tool_call::ToolRejection;
 use maka_agent_facts::tool_output::ToolOutput;
 use maka_agent_facts::tool_output::ToolSuccess;
@@ -59,21 +60,56 @@ pub(crate) struct SelectedTool {
     binding: String,
 }
 
+pub(crate) struct Catalog {
+    pub tools: ToolCatalog,
+    pub basis: Vec<SelectedTool>,
+    /// Derived from actual bound contracts and methods, never display names.
+    pub editing: Vec<(String, EditingTools)>,
+}
+
 pub(crate) fn catalog(
     services: NativeServices,
     store: Arc<EventLog>,
     selected: &[ServiceTool],
     additional: Vec<ToolRegistration>,
     workers: tokio_util::task::TaskTracker,
-) -> Result<(ToolCatalog, Vec<SelectedTool>), String> {
+) -> Result<Catalog, String> {
     let mut basis = Vec::new();
+    let file_contract = maka_workspace_api::files::contract();
+    let editing: Vec<_> = selected
+        .iter()
+        .filter_map(|selection| {
+            let (_, bindings) = services
+                .imports()
+                .find(|(name, _)| *name == selection.import)?;
+            let [binding] = bindings else {
+                return None;
+            };
+            if binding.contract() != &file_contract {
+                return None;
+            }
+            let style = match selection.method.as_str() {
+                "write" | "edit" => EditingTools::Structured,
+                "applyPatch" => EditingTools::ApplyPatch,
+                _ => return None,
+            };
+            Some((selection.name.clone(), style))
+        })
+        .collect();
     let mut web_bindings = Vec::new();
     let skill_contract = maka_skills_api::contract();
-    let tool_names: Arc<Vec<String>> = Arc::new(
+    let tool_names: Arc<Vec<(String, Option<EditingTools>)>> = Arc::new(
         selected
             .iter()
             .map(|tool| tool.name.clone())
             .chain(additional.iter().map(|tool| tool.definition.name.clone()))
+            .map(|name| {
+                let style = editing
+                    .iter()
+                    .find(|(editor, _)| *editor == name)
+                    .map(|(_, style)| *style);
+                (name, style)
+            })
             .collect(),
     );
     let registrations = selected
@@ -213,12 +249,16 @@ pub(crate) fn catalog(
             .with_request_binding([name].into(), binding)
             .map_err(|_| "agent_web_binding_invalid")?;
     }
-    Ok((catalog, basis))
+    Ok(Catalog {
+        tools: catalog,
+        basis,
+        editing,
+    })
 }
 #[derive(Clone)]
 struct Service {
     web_basis: Option<maka_web_api::contract::Basis>,
-    tool_names: Arc<Vec<String>>,
+    tool_names: Arc<Vec<(String, Option<EditingTools>)>>,
     services: NativeServices,
     store: Arc<EventLog>,
     binding: String,
@@ -288,14 +328,26 @@ impl ToolPreparer for Service {
                                 &context.invocation,
                             )
                             .await?;
+                            let editing = store
+                                .invocation_configuration(&context.invocation)
+                                .await
+                                .map_err(|error| ToolError::Failed(error.to_string()))?
+                                .ok_or_else(|| {
+                                    ToolError::Failed(
+                                        "Skill call has no invocation configuration".into(),
+                                    )
+                                })?
+                                .editing_tools;
                             let names: Vec<_> = tool_names
                                 .iter()
-                                .filter(|name| {
-                                    configuration
-                                        .tools
-                                        .as_ref()
-                                        .is_none_or(|allowed| allowed.contains(name))
+                                .filter(|(name, style)| {
+                                    (editing.is_none() || style.is_none() || editing == *style)
+                                        && configuration
+                                            .tools
+                                            .as_ref()
+                                            .is_none_or(|allowed| allowed.contains(name))
                                 })
+                                .map(|(name, _)| name)
                                 .collect();
                             serde_json::json!({"input": input, "tools": names, "workspace":configuration.workspace})
                         }
diff --git a/plugins/agent/tools/src/cell.rs b/plugins/agent/tools/src/cell.rs
index 1306555081..d28ee6dbbd 100644
--- a/plugins/agent/tools/src/cell.rs
+++ b/plugins/agent/tools/src/cell.rs
@@ -258,7 +258,7 @@ impl ToolExecutor for NestedTools {
                 }
             };
             let result = journal
-                .invoke_prepared_call(
+                .invoke_prepared_output(
                     operation_id,
                     call,
                     name.clone(),
@@ -267,7 +267,9 @@ impl ToolExecutor for NestedTools {
                     effect,
                 )
                 .await?;
-            Ok(result)
+            result
+                .into_code_value()
+                .map_err(|message| maka_agent_facts::tools::ToolError::Failed(message.into()))
         })
     }
 }
diff --git a/plugins/workspace/workspace-files/native/src/lib.rs b/plugins/workspace/workspace-files/native/src/lib.rs
index 3a9163568e..4f012f1391 100644
--- a/plugins/workspace/workspace-files/native/src/lib.rs
+++ b/plugins/workspace/workspace-files/native/src/lib.rs
@@ -181,6 +181,16 @@ impl Handler for Files {
     }
 }
 impl NativeOwner for Files {
+    fn execution_basis(&self) -> Option<Value> {
+        // Package/configuration identify the implementation; these are its
+        // captured targets. Comparison never restores a handle or grants access.
+        // Calls still read the live Session boundary from this exact Root.
+        let root = self.store.owned_root().ok()?;
+        Some(serde_json::json!({
+            "root": root.root_id(),
+            "privateRoot": self.state_root,
+        }))
+    }
     fn prepare_retirement(
         &self,
     ) -> Option<Box<dyn maka_host_runtime::application::NativeRetirement>> {
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 90d0f1282e..24684c43c8 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -111,7 +111,7 @@
     "plugins/agent/insights/Cargo.toml": "e11b391c1304bb27d552322e9f31b9e1858340f81f49ebbb3d484b02c9238def",
     "plugins/agent/model/Cargo.toml": "0fbd5b92911a7d3af54f10069d16b1b1b4b2d453b79aeb6ea63e66fd78d6ad85",
     "plugins/agent/model/tests/fixtures/provider-errors.mjs": "2efc38763493dbd5ad2d1f2869acde6ec450197a7ab62a01b5d4c6e3bb1d0518",
-    "plugins/agent/native/Cargo.toml": "4aaacf486f7e37d07fb320fb1c30bb574f180de3e5674d2a799b0b7f4f262226",
+    "plugins/agent/native/Cargo.toml": "1af18ae51a08acbe52ad0784488891f218b2d81eb5271e8ca85831f67d08242f",
     "plugins/agent/navigation/desktop.ts": "1debdbc123da7d06a11f4a77c22e5d7bfaaba392f34698d2447e9689ce6d87f0",
     "plugins/agent/navigation/package.ts": "7b1e3ed3be6f8f75238d3975001cadc943a1c6fabdcdde0300a382604e347544",
     "plugins/agent/navigation/tui.ts": "734105ea23900dbcbf1f2e75f92339f00b515e90937e49c28daca1afb3c67c1f",
diff --git a/products/maka/cli/src/application.rs b/products/maka/cli/src/application.rs
index 4cdc5ee968..eedbd0c09b 100644
--- a/products/maka/cli/src/application.rs
+++ b/products/maka/cli/src/application.rs
@@ -278,27 +278,6 @@ fn read_json<T: serde::de::DeserializeOwned>(source: &Path) -> Result<T, HostErr
     Ok(serde_json::from_slice(&bytes)?)
 }
 
-#[cfg(test)]
-mod source_input_tests {
-    use super::*;
-
-    #[test]
-    fn authoring_reads_large_contract_documents_with_the_deployment_bound() {
-        let directory = tempfile::tempdir().unwrap();
-        let file = directory.path().join("remote-exports.json");
-        let value = serde_json::json!({"contract": "x".repeat(768 * 1024)});
-        std::fs::write(&file, serde_json::to_vec(&value).unwrap()).unwrap();
-        assert_eq!(read_json::<serde_json::Value>(&file).unwrap(), value);
-        std::fs::write(&file, vec![b' '; 1024 * 1024 + 1]).unwrap();
-        assert!(
-            read_json::<serde_json::Value>(&file)
-                .unwrap_err()
-                .to_string()
-                .contains("1 MiB")
-        );
-    }
-}
-
 async fn open_store(root: PathBuf) -> Result<maka_event_log::EventLog, HostError> {
     let owner = tokio::task::spawn_blocking(move || {
         maka_event_log::root::RootOwner::open(
@@ -412,3 +391,24 @@ async fn run_tui(
         Err(error) => Err(format!("{}\nUI activation: {error}", startup_errors.join("\n")).into()),
     }
 }
+
+#[cfg(test)]
+mod source_input_tests {
+    use super::*;
+
+    #[test]
+    fn authoring_reads_large_contract_documents_with_the_deployment_bound() {
+        let directory = tempfile::tempdir().unwrap();
+        let file = directory.path().join("remote-exports.json");
+        let value = serde_json::json!({"contract": "x".repeat(768 * 1024)});
+        std::fs::write(&file, serde_json::to_vec(&value).unwrap()).unwrap();
+        assert_eq!(read_json::<serde_json::Value>(&file).unwrap(), value);
+        std::fs::write(&file, vec![b' '; 1024 * 1024 + 1]).unwrap();
+        assert!(
+            read_json::<serde_json::Value>(&file)
+                .unwrap_err()
+                .to_string()
+                .contains("1 MiB")
+        );
+    }
+}
diff --git a/products/maka/cli/src/code.rs b/products/maka/cli/src/code.rs
index fbbbad9287..014940fee2 100644
--- a/products/maka/cli/src/code.rs
+++ b/products/maka/cli/src/code.rs
@@ -173,6 +173,7 @@ pub(super) async fn run(args: Args) -> Result<(), maka_host_runtime::HostError>
             configuration: Some(Box::new(InvocationConfiguration {
                 workspace_origin: maka_agent_facts::execution::WorkspaceOrigin::Selected,
                 system_prompt: None,
+                editing_tools: None,
                 tool_discovery: false,
                 tool_composition: None,
                 workspace_identity: None,
diff --git a/products/maka/cli/tests/integration/application.rs b/products/maka/cli/tests/integration/application.rs
index abcc70f4f2..8048643097 100644
--- a/products/maka/cli/tests/integration/application.rs
+++ b/products/maka/cli/tests/integration/application.rs
@@ -174,7 +174,19 @@ fn url_install_commits_validated_bytes_without_changing_the_composition() {
                         .set_read_timeout(Some(std::time::Duration::from_secs(5)))
                         .unwrap();
                     let mut request = [0; 8192];
-                    stream.read(&mut request).unwrap();
+                    let mut received = 0;
+                    while !request[..received]
+                        .windows(4)
+                        .any(|bytes| bytes == b"\r\n\r\n")
+                    {
+                        assert!(
+                            received < request.len(),
+                            "package request headers exceed the fixture limit"
+                        );
+                        let read = stream.read(&mut request[received..]).unwrap();
+                        assert!(read > 0, "package request ended before its headers");
+                        received += read;
+                    }
                     write!(
                         stream,
                         "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org