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:01 UTC

(maka) 01/14: feat(agent): restore model Code Mode and nested service tools

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 5a4f67e2fab6c23aadffd87d52b3d4f7398a5163
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Oct 13 14:56:52 2026 +0800

    feat(agent): restore model Code Mode and nested service tools
---
 packages/plugin-sdk/src/configurations.ts          |  10 ++
 packages/plugin-sdk/src/generated/runtime.ts       |   5 +
 plugins/agent/api/src/contract.rs                  |   4 +
 plugins/agent/native/README.md                     |   2 +
 plugins/agent/native/README.zh-CN.md               |   2 +
 plugins/agent/native/src/model.rs                  |  13 +++
 plugins/agent/native/src/owner/execution.rs        |   5 +-
 plugins/agent/native/src/owner/queue.rs            |   1 +
 .../agent/native/src/owner/tests/attachments.rs    |   2 +-
 plugins/agent/native/src/owner/tests/selection.rs  |  65 +++++++++++
 .../agent/native/src/owner/tests/service_tools.rs  | 124 +++++++++++++++++++++
 plugins/agent/native/src/tools.rs                  |   6 +-
 products/maka/bundle/src/installation.ts           |  10 +-
 products/maka/cli/notices.json                     |   6 +-
 14 files changed, 248 insertions(+), 7 deletions(-)

diff --git a/packages/plugin-sdk/src/configurations.ts b/packages/plugin-sdk/src/configurations.ts
index ec4799858b..53f9b89d86 100644
--- a/packages/plugin-sdk/src/configurations.ts
+++ b/packages/plugin-sdk/src/configurations.ts
@@ -337,6 +337,11 @@ export const configurations = {
       AgentServiceTool: {
         type: 'object',
         properties: {
+          nestable: {
+            description:
+              'Allow this selected service inside Code Mode. Binding and permission\nchecks are identical to a direct call; omitted keeps direct-only behavior.',
+            type: 'boolean',
+          },
           input: {
             description:
               'Explicit input projection. Skill tools receive the actual Agent tool names,\noutside model-controlled arguments; this is compatibility data, not authority.',
@@ -796,6 +801,11 @@ export const configurations = {
         type: 'object',
         additionalProperties: false,
         properties: {
+          nestable: {
+            type: 'boolean',
+            description:
+              'Allow this selected service inside Code Mode. Binding and permission\nchecks are identical to a direct call; omitted keeps direct-only behavior.',
+          },
           input: {
             $ref: '#/$defs/AgentToolInput',
             description:
diff --git a/packages/plugin-sdk/src/generated/runtime.ts b/packages/plugin-sdk/src/generated/runtime.ts
index 5eaf30baf4..6d1a06f0bb 100644
--- a/packages/plugin-sdk/src/generated/runtime.ts
+++ b/packages/plugin-sdk/src/generated/runtime.ts
@@ -6644,6 +6644,11 @@ export interface DirectoryRootSpec {
   path: string;
 }
 export interface AgentServiceTool {
+  /**
+   * Allow this selected service inside Code Mode. Binding and permission
+   * checks are identical to a direct call; omitted keeps direct-only behavior.
+   */
+  nestable?: boolean;
   /**
    * Explicit input projection. Skill tools receive the actual Agent tool names,
    * outside model-controlled arguments; this is compatibility data, not authority.
diff --git a/plugins/agent/api/src/contract.rs b/plugins/agent/api/src/contract.rs
index bc024e44ce..7899c5b5cc 100644
--- a/plugins/agent/api/src/contract.rs
+++ b/plugins/agent/api/src/contract.rs
@@ -105,6 +105,10 @@ pub struct AssistantConfiguration {
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
 #[schemars(rename = "AgentServiceTool")]
 pub struct ServiceTool {
+    /// Allow this selected service inside Code Mode. Binding and permission
+    /// checks are identical to a direct call; omitted keeps direct-only behavior.
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub nestable: bool,
     /// Explicit input projection. Skill tools receive the actual Agent tool names,
     /// outside model-controlled arguments; this is compatibility data, not authority.
     #[serde(default)]
diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md
index 6c65d5b6d6..32cb91fc36 100644
--- a/plugins/agent/native/README.md
+++ b/plugins/agent/native/README.md
@@ -25,4 +25,6 @@ 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.
+
 [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 a2cd93d858..c348f59eb6 100644
--- a/plugins/agent/native/README.zh-CN.md
+++ b/plugins/agent/native/README.zh-CN.md
@@ -25,4 +25,6 @@
 
 服务管理自己的激活,并委托 `maka-agent-engine` 执行运行。会话历史、控制和工具调用使用获准作用域与原始操作回执。文件、进程、技能等服务都是通过组合显式选择的依赖。
 
+Agent 接收消息时保存模型的 Code Mode 默认值或显式 `codeMode` 覆盖设置;排队消息和恢复运行保留原模式。服务工具可以通过 `nestable: true` 允许代码内调用,仍执行与直接调用相同的权限检查、操作回执和清理流程。省略该字段时仅允许直接调用。Maka 默认组合为文件、进程、Web 和 Skills 工具启用嵌套调用。
+
 [源码](src/lib.rs) · [Cargo 配置](Cargo.toml)
diff --git a/plugins/agent/native/src/model.rs b/plugins/agent/native/src/model.rs
index f340c8e4bf..92d15ec204 100644
--- a/plugins/agent/native/src/model.rs
+++ b/plugins/agent/native/src/model.rs
@@ -27,6 +27,7 @@ use maka_agent_facts::configuration::ModelInfo;
 use maka_agent_facts::configuration::ModelSelection;
 use maka_agent_facts::execution::ModelBinding;
 use maka_agent_facts::execution::ThinkingLevel;
+use maka_agent_facts::execution::ToolMode;
 use maka_agent_ports::provider::Connection;
 use maka_agent_ports::provider::Definition;
 use maka_agent_ports::provider::Model;
@@ -69,6 +70,10 @@ impl ExecutionBasis {
 pub(crate) struct AcceptedModel {
     pub binding: ModelBinding,
     pub thinking_level: Option<ThinkingLevel>,
+    /// Execution policy is captured at message admission. Later model settings
+    /// do not reinterpret queued work. Older accepted messages remain Direct.
+    #[serde(default)]
+    pub tool_mode: ToolMode,
     provider: maka_agent_facts::provider::Identity,
     provider_configuration: Value,
     basis: ExecutionBasis,
@@ -109,6 +114,14 @@ impl Source {
             configuration,
             credentials,
             accepted: AcceptedModel {
+                tool_mode: ToolMode::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.code_mode),
+                ),
                 binding: ModelBinding {
                     connection_id: row.connection_id.clone(),
                     connection_slug: row.slug.clone(),
diff --git a/plugins/agent/native/src/owner/execution.rs b/plugins/agent/native/src/owner/execution.rs
index 0a40fa7a56..26452d6ad5 100644
--- a/plugins/agent/native/src/owner/execution.rs
+++ b/plugins/agent/native/src/owner/execution.rs
@@ -274,7 +274,7 @@ impl Owner {
         {
             return Ok(parked(Reason::WorkspaceChanged));
         }
-        let (source, prepared) = crate::model::Source::select(
+        let (mut source, prepared) = crate::model::Source::select(
             self.configuration.clone(),
             self.credentials.clone(),
             input.model,
@@ -284,6 +284,9 @@ impl Owner {
         )
         .await
         .map_err(public_error)?;
+        // 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;
         let basis = MessageBasis {
             allowed_tools: session.configuration.tools.clone(),
             background: None,
diff --git a/plugins/agent/native/src/owner/queue.rs b/plugins/agent/native/src/owner/queue.rs
index 4da2d85193..e0faae038a 100644
--- a/plugins/agent/native/src/owner/queue.rs
+++ b/plugins/agent/native/src/owner/queue.rs
@@ -324,6 +324,7 @@ impl Owner {
                 let (source, prepared) = *value;
                 configuration.model = Some(source.accepted.binding.clone());
                 configuration.thinking_level = source.accepted.thinking_level;
+                configuration.tool_mode = source.accepted.tool_mode;
                 PreparedRun::Model(Box::new(RunInput {
                     model_source: Some(Arc::new(source)),
                     model_revision: Some(prepared.revision),
diff --git a/plugins/agent/native/src/owner/tests/attachments.rs b/plugins/agent/native/src/owner/tests/attachments.rs
index e926a19aca..536cf248b6 100644
--- a/plugins/agent/native/src/owner/tests/attachments.rs
+++ b/plugins/agent/native/src/owner/tests/attachments.rs
@@ -192,7 +192,7 @@ async fn attachment_payload_validation_and_image_read_use_actual_bytes() {
         let clipboard = f.mutation_with(2, "uploadAttachment", json!({"sessionId":session,"name":descriptor["name"],"bytes":descriptor["bytes"],"digest":descriptor["digest"]}), Uuid::new_v4(),
             BTreeMap::from([("file".into(), snapshot as Arc<dyn ReadResource>)])).await.unwrap();
         assert_eq!(clipboard["attachment"]["mimeType"], "image/png");
-        assert_eq!(budget.usage(), (0, 0, 0));
+        assert_eq!(budget.usage(), (0, 0));
         assert!(!f._dir.path().join("clipboard.png").exists());
         f.mutation(2, "send", json!({"sessionId":session,"message":"","attachments":[attachment["id"],clipboard["attachment"]["id"]],"model":f.selection().await}), Uuid::new_v4()).await.unwrap();
         let (mut socket, _) = listener.accept().await.unwrap();
diff --git a/plugins/agent/native/src/owner/tests/selection.rs b/plugins/agent/native/src/owner/tests/selection.rs
index ada42c587f..c238f4a678 100644
--- a/plugins/agent/native/src/owner/tests/selection.rs
+++ b/plugins/agent/native/src/owner/tests/selection.rs
@@ -708,3 +708,68 @@ async fn invalid_thinking_default_remains_unavailable_without_poisoning_other_mo
     fixture.app.shutdown().await.unwrap();
     fixture.config.shutdown().await.unwrap();
 }
+
+#[tokio::test]
+async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain_direct() {
+    let fixture = Fixture::new("http://127.0.0.1:1/v1", 1, None).await;
+    let session = fixture.create("Frozen tool policy").await;
+    edit(&fixture, |changes| {
+        changes.model_overrides = Patch::Set(BTreeMap::from([(
+            "fixture-model".into(),
+            ModelOverride {
+                code_mode: Some(true),
+                ..Default::default()
+            },
+        )]));
+    })
+    .await;
+    let (source, _) = crate::model::Source::select(
+        fixture.config.clone(),
+        fixture.owner.credentials.clone(),
+        fixture.selection().await,
+        None,
+        session.clone(),
+        fixture.owner.revision.clone(),
+    )
+    .await
+    .unwrap();
+    assert_eq!(source.accepted.tool_mode, ToolMode::CodeMode);
+    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),
+                ..Default::default()
+            },
+        )]));
+    })
+    .await;
+    let restored = crate::model::Source::restore(
+        fixture.config.clone(),
+        fixture.owner.credentials.clone(),
+        serde_json::from_value(stored.clone()).unwrap(),
+        session.clone(),
+        fixture.owner.revision.clone(),
+    )
+    .unwrap();
+    restored.capture().await.unwrap();
+    assert_eq!(restored.accepted.tool_mode, ToolMode::CodeMode);
+    let (next, _) = crate::model::Source::select(
+        fixture.config.clone(),
+        fixture.owner.credentials.clone(),
+        fixture.selection().await,
+        None,
+        session.clone(),
+        fixture.owner.revision.clone(),
+    )
+    .await
+    .unwrap();
+    assert_eq!(next.accepted.tool_mode, ToolMode::Direct);
+    let mut legacy = stored;
+    legacy.as_object_mut().unwrap().remove("tool_mode");
+    let accepted: crate::model::AcceptedModel = serde_json::from_value(legacy).unwrap();
+    assert_eq!(accepted.tool_mode, ToolMode::Direct);
+    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 f7b38d83d0..db0a90a84c 100644
--- a/plugins/agent/native/src/owner/tests/service_tools.rs
+++ b/plugins/agent/native/src/owner/tests/service_tools.rs
@@ -133,6 +133,7 @@ pub(super) struct Setup {
     title: Option<maka_agent_facts::display::Text>,
     counter: Option<Arc<Counter>>,
     granted: bool,
+    nestable: bool,
     query: bool,
     questions: bool,
     forms: bool,
@@ -178,6 +179,7 @@ impl Setup {
             preparation: None,
             images: None,
             confirmation: contract::ToolConfirmation::Preapproved,
+            nestable: false,
         }
     }
     fn native(counter: Arc<Counter>, granted: bool) -> Self {
@@ -202,6 +204,7 @@ impl Setup {
             preparation: None,
             images: None,
             confirmation: contract::ToolConfirmation::Preapproved,
+            nestable: false,
         }
     }
     pub(super) fn forms() -> Self {
@@ -232,6 +235,7 @@ impl Setup {
             preparation: None,
             images: None,
             confirmation: contract::ToolConfirmation::Preapproved,
+            nestable: false,
         }
     }
     pub(super) fn files(method: &'static str) -> Self {
@@ -429,6 +433,7 @@ impl Setup {
     }
     pub(super) fn selection(&self) -> contract::ServiceTool {
         contract::ServiceTool {
+            nestable: self.nestable,
             input: if self.web {
                 contract::ToolInput::Web
             } else {
@@ -1724,6 +1729,11 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
                 &format!("http://{}/v1", listener.local_addr().unwrap()), 1, None, None,
                 Some(Setup::native(counter.clone(), true)),
             ).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("Resume settled tool").await;
             let sent = fixture.send(&session).await.unwrap();
             let (mut first, _) = listener.accept().await.unwrap();
@@ -1736,6 +1746,11 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
             fixture.drained().await;
             let source = fixture.query("execution", json!({"sessionId":session,"invocationId":sent["invocationId"]})).await.unwrap();
             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()
+                })]));
+            }).await;
             let resumed = fixture.mutation(2, "resume", json!({
                 "sessionId":session, "sourceInvocationId":sent["invocationId"],
                 "sourceHighWater":source["execution"]["highWater"], "model":fixture.selection().await
@@ -1743,6 +1758,7 @@ async fn resume_replays_original_settlements_without_redispatching_effects() {
             assert_eq!(resumed["kind"], "started", "{resumed}");
             let (mut replay, _) = listener.accept().await.unwrap();
             let prompt = request(&mut replay).await;
+            assert!(prompt["tools"].as_array().unwrap().iter().any(|tool| tool["function"]["name"] == "exec"), "resume retains the original Code Mode despite changed model settings");
             let result = prompt["messages"].as_array().unwrap().iter().find(|message| message["role"] == "tool").unwrap();
             if matches!(behavior, Behavior::Returned) {
                 assert_eq!(serde_json::from_str::<Value>(result["content"].as_str().unwrap()).unwrap()["effects"], 1);
@@ -1802,3 +1818,111 @@ async fn session_tool_selection_is_an_execution_ceiling_and_is_inherited() {
         fixture.config.shutdown().await.unwrap();
     }).await.unwrap();
 }
+
+#[tokio::test]
+async fn code_mode_calls_selected_native_services_with_original_grants_and_receipts() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        for (nestable, granted) in [(true, true), (true, false), (false, true)] {
+            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let base = format!("http://{}/v1", listener.local_addr().unwrap());
+            let counter = Counter::new(Behavior::Returned);
+            let mut setup = Setup::native(counter.clone(), granted);
+            setup.nestable = nestable;
+            let fixture = Fixture::with_services(&base, 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), ..Default::default()
+                })]));
+            }).await;
+            let server = tokio::spawn(async move {
+                let (mut socket, _) = listener.accept().await.unwrap();
+                let first = request(&mut socket).await;
+                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}));";
+                code_response(&mut socket, code).await;
+                let (mut socket, _) = listener.accept().await.unwrap();
+                let next = request(&mut socket).await;
+                let result = next["messages"].as_array().unwrap().iter().find(|message| message["role"] == "tool").unwrap();
+                assert_eq!(result["tool_call_id"], "code-call");
+                if nestable && granted {
+                    assert!(result["content"].as_str().unwrap().contains("effects"));
+                }
+                response(&mut socket).await;
+            });
+            let session = fixture.create("Code Mode native services").await;
+            fixture.send(&session).await.unwrap();
+            fixture.drained().await;
+            server.await.unwrap();
+            assert_eq!(counter.effects.load(Ordering::SeqCst), if nestable && granted { 2 } else { 0 });
+            let recorded = facts(&fixture, &session).await;
+            assert!(recorded.iter().any(|fact| matches!(fact, Fact::InvocationOpened { configuration: Some(configuration), .. } if configuration.tool_mode == ToolMode::CodeMode)));
+            if nestable && granted {
+                assert_eq!(recorded.iter().filter(|fact| matches!(fact, Fact::ToolDispatched { name, .. } if name == "Counter")).count(), 2);
+                let operations: Vec<_> = recorded.iter().filter_map(|fact| match fact {
+                    Fact::ToolDispatched { name, operation_id, .. } if name == "Counter" => Some(operation_id),
+                    _ => None,
+                }).collect();
+                assert_ne!(operations[0], operations[1]);
+                for expected in operations {
+                    assert_eq!(recorded.iter().filter(|fact| matches!(fact,
+                        Fact::ToolSettled { operation_id, outcome: ToolOutcome::Succeeded { .. } }
+                        if operation_id == expected
+                    )).count(), 1, "each nested service has its own successful durable settlement");
+                }
+            }
+            fixture.app.shutdown().await.unwrap();
+            fixture.config.shutdown().await.unwrap();
+        }
+    }).await.unwrap();
+}
+
+async fn code_response(socket: &mut TcpStream, code: &str) {
+    let delta = json!({"id":"code-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"code-call","type":"function","function":{"name":"exec","arguments":json!({"code":code}).to_string()}}]},"finish_reason":null}]});
+    let body = format!(
+        "data: {delta}\n\ndata: {{\"id\":\"code-model\",\"choices\":[{{\"index\":0,\"delta\":{{}},\"finish_reason\":\"tool_calls\"}}]}}\n\ndata: [DONE]\n\n"
+    );
+    socket.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
+}
+
+#[tokio::test]
+async fn code_mode_native_file_writes_obey_the_live_session_permission_boundary() {
+    tokio::time::timeout(Duration::from_secs(60), async {
+        for writable in [true, false] {
+            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let base = format!("http://{}/v1", listener.local_addr().unwrap());
+            let mut setup = Setup::files("write");
+            setup.nestable = true;
+            let fixture = Fixture::with_services(&base, 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), ..Default::default()
+                })]));
+            }).await;
+            let session = fixture.create("Code Mode files").await;
+            if !writable {
+                files::change(&fixture, &session, "read_only", false, true).await;
+            }
+            fixture.send(&session).await.unwrap();
+            let (mut first, _) = listener.accept().await.unwrap();
+            let prompt = request(&mut first).await;
+            assert!(prompt["tools"].as_array().unwrap().iter().any(|tool| tool["function"]["name"] == "exec"));
+            code_response(&mut first, "text(await tools.WorkspaceFile({path:'code-mode.txt',content:'native Code Mode write'}));").await;
+            let (mut next, _) = listener.accept().await.unwrap();
+            let observed = request(&mut next).await;
+            let tool = observed["messages"].as_array().unwrap().iter().find(|message| message["role"] == "tool").unwrap();
+            assert_eq!(tool["tool_call_id"], "code-call");
+            let target = fixture.workspace.join("code-mode.txt");
+            assert_eq!(target.exists(), writable);
+            if writable {
+                assert_eq!(std::fs::read_to_string(target).unwrap(), "native Code Mode write");
+            }
+            assert!(fixture.store.pending_interactions(&session).await.unwrap().is_empty());
+            response(&mut next).await;
+            fixture.drained().await;
+            fixture.app.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 00b6d7c83d..3da5f62b3a 100644
--- a/plugins/agent/native/src/tools.rs
+++ b/plugins/agent/native/src/tools.rs
@@ -192,7 +192,11 @@ pub(crate) fn catalog(
                     },
                     freeform: None,
                 },
-                nesting: ToolNesting::DirectOnly,
+                nesting: if selection.nestable {
+                    ToolNesting::Nestable
+                } else {
+                    ToolNesting::DirectOnly
+                },
                 semantics: if method.mode == MethodMode::Mutation {
                     ToolSemantics::ExclusiveStep
                 } else {
diff --git a/products/maka/bundle/src/installation.ts b/products/maka/bundle/src/installation.ts
index 962f344561..6cf6644bc3 100644
--- a/products/maka/bundle/src/installation.ts
+++ b/products/maka/bundle/src/installation.ts
@@ -44,7 +44,15 @@ export function installation(
     description: string,
     result: 'json' | 'image' = 'json',
   ) =>
-    tools.push({ import: service, method, name, description, result, confirmation: 'preapproved' });
+    tools.push({
+      import: service,
+      method,
+      name,
+      description,
+      result,
+      confirmation: 'preapproved',
+      nestable: ['files', 'processes', 'web', 'skills'].includes(service),
+    });
   add('files', 'readText', 'Read', 'Read text from a file in the selected workspace.');
   add('files', 'readImage', 'ReadImage', 'Read an image in the selected workspace.', 'image');
   add('files', 'glob', 'Glob', 'Find files in the selected workspace.');
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index aa194fa09b..7453a51e69 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -4,9 +4,9 @@
     "Cargo.toml": "83d55b86f5c48c71bc65ff6f3a235d166e56c050ffcc1027fe390a836aee3a11",
     "package-lock.json": "a5af15aca21fabe0e1bdd48784167df4dc81cee17777d54861dcbd641168a4eb",
     "package.json": "38baf3beeb5876849ca16e31868d59bfaee769f260c7555b4a8a1847b92e28a1",
-    "packages/plugin-sdk/src/configurations.ts": "ec70531da1097cae8d3835fcdeab73e2354e955b807a25f3cdbdab35005297a1",
+    "packages/plugin-sdk/src/configurations.ts": "2da8c45bdfc3fdfc1239e956d9eb5ce4e3acc42e8c40fe5e4896c4b4d35f83e1",
     "packages/plugin-sdk/src/contracts.ts": "63010ab6d927cf45831d5d02afe93efef07c785b0a5cfbd09ed857a99d7d3118",
-    "packages/plugin-sdk/src/generated/runtime.ts": "d2407ac687fe751518769ac380c4ac599bfc184f224642330bba19515afb8709",
+    "packages/plugin-sdk/src/generated/runtime.ts": "ac2d5f68ef4cde2d8ac560d7b6a98f43aebf1da87aadf56da50e4247d7418607",
     "packages/plugin-sdk/src/index.ts": "27b68be5a29d1857f7fe361c8c678e77ba21413c79ec3870c1ce30d0faf9323f",
     "packages/plugin-sdk/src/native.ts": "e207147ef7d3e2c39533c1ff24aae942c86da5350f8b282bae0d4fb417bbf37b",
     "packages/plugin-sdk/src/ui.ts": "d6c64e9406c41ad6bbda73435b559aad24c31adb3c5bbb4ab6f49fff8a8330ca",
@@ -365,7 +365,7 @@
     "products/maka/bundle/package.json": "dd17e0a82ab68c03cdfd8adb3e243d4c5e881530db66ff311cb642b3bdc1f7c0",
     "products/maka/bundle/scripts/build.mjs": "db11d8cd6f29ef7ead087261848568da1ad3889a870a89f23fa969449d769d47",
     "products/maka/bundle/src/composition.ts": "d5e8142d7839654ffa732e99f41bca1c19c364df20529de6f1e56db4ec880198",
-    "products/maka/bundle/src/installation.ts": "25908ab7c6a6416fa2c452e9f25840cb9c542c6b99a7eb28ea71725adf220738",
+    "products/maka/bundle/src/installation.ts": "73cee5001b1ec45f45254932a9497ec1570f15dcee33de24a6a90ae4ce4deb6f",
     "products/maka/cli/Cargo.toml": "dff55e4f17eecb5107b75c179e0077f122409503993d19bd24c70e5c9380ffba",
     "products/maka/cli/build.rs": "98452274bb0f20fa55fda5a20313f81740e759652aa4227fba0b0de5e3f4cc29",
     "products/maka/cli/build/product.rs": "02555a9bd0181080a061fe35b1b1caabec6c2dc338a35923b49f59f010892a6c",


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