diff --git a/MiniApp/Skills/miniapp-dev/SKILL.md b/MiniApp/Skills/miniapp-dev/SKILL.md index 9ce45037d2..e98a466cec 100644 --- a/MiniApp/Skills/miniapp-dev/SKILL.md +++ b/MiniApp/Skills/miniapp-dev/SKILL.md @@ -193,7 +193,7 @@ MiniApp 框架**只暴露下列能力**,没有任何"通用 BitFun 后端通 | AI | `app.ai.complete / chat / cancel / getModels` | 复用宿主 AIClient,受 `permissions.ai`(含 `allowed_models` / 速率限制) | | 对话框 | `app.dialog.open/save/message` | Tauri dialog 插件 | | 剪贴板 | `app.clipboard.readText/writeText` | 宿主 navigator.clipboard | -| Agent 会话 | `app.agent.run / cancel / turnText / cancelStaleRuns / onEvent` | 受 `permissions.agent.enabled` 限制;启动小应用自己的隐藏 agent 回合,事件只回流到发起的小应用 | +| Agent 会话 | `app.agent.run / cancel / turnText / cancelStaleRuns / onEvent` | 受 `permissions.agent.enabled` 限制;启动小应用自己的隐藏 agent 回合,事件只回流到发起的小应用。工具集按运行时档位收敛:市场小应用(`runtime_profile = market_strict`)只保留 `WebSearch` / `WebFetch` 这类只读联网调研工具,碰不到文件系统、命令行和宿主控制面;内置 / `compatibility` 档位保留完整的 headless 工具集 | | 悬浮会话气泡 | `app.chat.claimComposer / releaseComposer / focusSession / setComposerDraft / onUserMessage` | 受 `permissions.agent.enabled` 限制;把内容和提交路由注册进右下角的标准悬浮聊天窗(输入器、附件、模型、权限、停止等仍由宿主共享组件拥有),并展示小应用自己的 Agent 过程(Agentic MiniApp 模式,样板间:`builtin-ppt-live`) | | 幻灯片栅格化 | `app.deck.renderPage` | 在隐藏宿主 WebView 中渲染单页 HTML,返回 base64 PNG/PDF(导出用) | | 自定义后端 | `app.call('xxx', …)` + `worker.js` | 仅 `node.enabled = true` 时可用,自己实现业务逻辑 | diff --git a/src/apps/desktop/src/api/miniapp_agent_api.rs b/src/apps/desktop/src/api/miniapp_agent_api.rs index 24264a9630..3e389f81ee 100644 --- a/src/apps/desktop/src/api/miniapp_agent_api.rs +++ b/src/apps/desktop/src/api/miniapp_agent_api.rs @@ -264,6 +264,23 @@ async fn load_and_validate_miniapp_agent_session( Ok(Some(session)) } +/// Align a reused hidden session with the tool policy of the current run. +/// +/// `enable_tools` is baked into the session config at creation time, so sessions +/// created by older builds (which disabled tools for marketplace MiniApps) would +/// stay tool-less forever. Marketplace runs are now constrained by the backend +/// research allowlist instead, so the session config is repaired on reuse. +async fn sync_agent_session_tool_enablement( + coordinator: &ConversationCoordinator, + session_id: &str, + submission_plan: &MiniAppAgentSubmissionPlan, +) -> Result<(), String> { + coordinator + .update_session_tool_enablement(session_id, submission_plan.enable_tools) + .await + .map_err(|e| format!("Failed to update MiniApp agent session tools: {}", e)) +} + /// Ensure that one MiniApp topic has a dedicated hidden Agent session before /// the user opens its floating chat surface. This command intentionally accepts /// only an appdata-relative workspace, so it remains a local-host capability @@ -298,6 +315,10 @@ pub async fn miniapp_agent_ensure_session( request.session_id.as_deref(), &workspace_plan.workspace_path, request.enable_tools, + state + .miniapp_manager + .uses_market_strict_runtime(&request.app_id) + .await, ); let requested_model = request .model @@ -324,6 +345,12 @@ pub async fn miniapp_agent_ensure_session( .await .map_err(|e| format!("Failed to update MiniApp agent session model: {}", e))?; } + sync_agent_session_tool_enablement( + coordinator.inner().as_ref(), + &existing_session_id, + &submission_plan, + ) + .await?; (existing_session_id, false) } else { check_agent_rate_limit( @@ -412,6 +439,10 @@ pub async fn miniapp_agent_run( request.session_id.as_deref(), &workspace_path, request.enable_tools, + state + .miniapp_manager + .uses_market_strict_runtime(&request.app_id) + .await, ); let requested_model = request @@ -439,6 +470,12 @@ pub async fn miniapp_agent_run( .await .map_err(|e| format!("Failed to update MiniApp agent session model: {}", e))?; } + sync_agent_session_tool_enablement( + coordinator.inner().as_ref(), + &existing_session_id, + &submission_plan, + ) + .await?; existing_session_id } else { // One hidden session per task keeps MiniApp work isolated and out of diff --git a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs index 7ec516fd33..f0da586436 100644 --- a/src/crates/assembly/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/assembly/core/src/agentic/coordination/coordinator.rs @@ -47,7 +47,7 @@ use crate::agentic::skill_agent_snapshot::{ }; use crate::agentic::tools::pipeline::{SubagentParentInfo, ToolPipeline}; use crate::agentic::tools::{ - is_miniapp_headless_agent_run, miniapp_headless_agent_tool_restrictions, + miniapp_agent_run_tool_restrictions, tool_restrictions_for_delegation_policy as runtime_tool_restrictions_for_delegation_policy, ToolRuntimeRestrictions, }; @@ -2117,6 +2117,31 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet Ok(()) } + /// Re-enable (or disable) the tool loop of an already persisted session. + /// + /// Session configs are written once at creation, so a host that changes its + /// tool policy would otherwise only affect newly created sessions. + pub async fn update_session_tool_enablement( + &self, + session_id: &str, + enable_tools: bool, + ) -> BitFunResult<()> { + self.ensure_session_runtime_ownership(session_id, None)?; + + if self + .session_manager + .update_session_tool_enablement(session_id, enable_tools) + .await? + { + info!( + "Coordinator updated session tool enablement: session_id={}, enable_tools={}", + session_id, enable_tools + ); + } + + Ok(()) + } + /// Common creation entry point for normal persisted sessions. /// /// Delegated subagent sessions use the hidden-subagent creation path instead. @@ -4506,14 +4531,13 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet { skill_agent_context_vars.insert("acp_transport".to_string(), "true".to_string()); } - let runtime_tool_restrictions = if is_miniapp_headless_agent_run( + // Marketplace MiniApps are third-party code, so their hidden agent turns + // run on a read-only research allowlist rather than the wider built-in + // MiniApp tool set. + let runtime_tool_restrictions = miniapp_agent_run_tool_restrictions( user_message_metadata.as_ref(), session.created_by.as_deref(), - ) { - miniapp_headless_agent_tool_restrictions() - } else { - ToolRuntimeRestrictions::default() - }; + ); let runtime_tool_restrictions = runtime_tool_restrictions_for_session_lifetime( runtime_tool_restrictions, self.session_manager.is_transient_session(&session_id), diff --git a/src/crates/assembly/core/src/agentic/session/session_manager.rs b/src/crates/assembly/core/src/agentic/session/session_manager.rs index add3dab667..35d8b790cf 100644 --- a/src/crates/assembly/core/src/agentic/session/session_manager.rs +++ b/src/crates/assembly/core/src/agentic/session/session_manager.rs @@ -3441,6 +3441,50 @@ impl SessionManager { Ok(()) } + /// Update whether a session runs its tool loop. + /// + /// `enable_tools` is persisted per session, so a session created while the + /// caller disabled tools stays tool-less forever on reuse. Hosts that later + /// change their mind (for example MiniApp runs that moved from a frontend + /// switch to a backend allowlist) call this to repair existing sessions. + pub async fn update_session_tool_enablement( + &self, + session_id: &str, + enable_tools: bool, + ) -> BitFunResult { + let _mutation_guard = self.acquire_session_mutation(session_id).await?; + if let Some(mut session) = self.sessions.get_mut(session_id) { + if session.config.enable_tools == enable_tools { + return Ok(false); + } + session.config.enable_tools = enable_tools; + session.updated_at = SystemTime::now(); + session.last_activity_at = SystemTime::now(); + } else { + return Err(BitFunError::NotFound(format!( + "Session not found: {}", + session_id + ))); + } + + if self.should_persist_session_id(session_id) { + let effective_path = self.effective_session_storage_path(session_id).await; + let session_snapshot = self.sessions.get(session_id).map(|s| s.clone()); + if let (Some(workspace_path), Some(session)) = (effective_path, session_snapshot) { + self.persistence_manager + .save_session(&workspace_path, &session) + .await?; + } + } + + debug!( + "Session tool enablement updated: session_id={}, enable_tools={}", + session_id, enable_tools + ); + + Ok(true) + } + /// Inherit parent dialog mode state when creating forked child sessions. /// /// `last_user_dialog_agent_type` drives first-entry mode reminders, while diff --git a/src/crates/assembly/core/src/agentic/tools/mod.rs b/src/crates/assembly/core/src/agentic/tools/mod.rs index fad9e9a27d..3ebd0d2c21 100644 --- a/src/crates/assembly/core/src/agentic/tools/mod.rs +++ b/src/crates/assembly/core/src/agentic/tools/mod.rs @@ -42,7 +42,8 @@ pub use registry::{ get_readonly_registered_tool_names, get_readonly_tools, }; pub use restrictions::{ - is_miniapp_headless_agent_run, miniapp_headless_agent_tool_restrictions, - tool_restrictions_for_delegation_policy, ToolPathOperation, ToolPathPolicy, - ToolRuntimeRestrictions, + is_miniapp_headless_agent_run, is_miniapp_market_strict_agent_run, + miniapp_agent_run_tool_restrictions, miniapp_headless_agent_tool_restrictions, + miniapp_market_strict_agent_tool_restrictions, tool_restrictions_for_delegation_policy, + ToolPathOperation, ToolPathPolicy, ToolRuntimeRestrictions, }; diff --git a/src/crates/assembly/core/src/agentic/tools/restrictions.rs b/src/crates/assembly/core/src/agentic/tools/restrictions.rs index 59cd3b4a6e..8c58886659 100644 --- a/src/crates/assembly/core/src/agentic/tools/restrictions.rs +++ b/src/crates/assembly/core/src/agentic/tools/restrictions.rs @@ -1,8 +1,10 @@ use crate::util::errors::{BitFunError, BitFunResult}; pub use bitfun_agent_tools::{ - is_miniapp_headless_agent_run, is_remote_posix_path_within_root, - miniapp_headless_agent_tool_restrictions, tool_restrictions_for_delegation_policy, - ToolPathOperation, ToolPathPolicy, ToolRestrictionError, ToolRuntimeRestrictions, + is_miniapp_headless_agent_run, is_miniapp_market_strict_agent_run, + is_remote_posix_path_within_root, miniapp_agent_run_tool_restrictions, + miniapp_headless_agent_tool_restrictions, miniapp_market_strict_agent_tool_restrictions, + tool_restrictions_for_delegation_policy, ToolPathOperation, ToolPathPolicy, + ToolRestrictionError, ToolRuntimeRestrictions, }; use std::path::{Path, PathBuf}; diff --git a/src/crates/contracts/product-domains/src/miniapp/agent_bridge.rs b/src/crates/contracts/product-domains/src/miniapp/agent_bridge.rs index 8995842cad..560a92da92 100644 --- a/src/crates/contracts/product-domains/src/miniapp/agent_bridge.rs +++ b/src/crates/contracts/product-domains/src/miniapp/agent_bridge.rs @@ -284,13 +284,23 @@ pub fn validate_reused_session( Ok(()) } -pub fn agent_run_metadata(app_id: &str, run_id: &str) -> serde_json::Value { - json!({ +/// Metadata carried by every MiniApp agent turn. +/// +/// `market_strict` mirrors the app's runtime profile so the agent runtime can +/// pick the marketplace tool allowlist without reaching back into MiniApp +/// storage. The flag is only serialized for strict apps, keeping the turn +/// metadata of built-in MiniApps byte-identical to earlier releases. +pub fn agent_run_metadata(app_id: &str, run_id: &str, market_strict: bool) -> serde_json::Value { + let mut metadata = json!({ "surface": MINIAPP_AGENT_SURFACE, "appId": app_id, "runId": run_id, "acp_transport": true, - }) + }); + if market_strict { + metadata["marketStrict"] = Value::Bool(true); + } + metadata } pub fn build_agent_submission_plan( @@ -300,6 +310,7 @@ pub fn build_agent_submission_plan( requested_session: Option<&str>, workspace_path: &str, enable_tools: Option, + market_strict: bool, ) -> MiniAppAgentSubmissionPlan { MiniAppAgentSubmissionPlan { run_id: run_id.to_string(), @@ -308,7 +319,7 @@ pub fn build_agent_submission_plan( requested_session_id: requested_session_id(requested_session), workspace_path: workspace_path.to_string(), enable_tools: enable_tools.unwrap_or(true), - metadata: agent_run_metadata(app_id, run_id), + metadata: agent_run_metadata(app_id, run_id, market_strict), } } @@ -475,6 +486,7 @@ mod tests { Some(" session-1 "), "/workspace", Some(false), + false, ); assert_eq!(plan.owner, "miniapp-agent:app-1:run-1"); assert_eq!(plan.session_name, "Session"); @@ -492,6 +504,25 @@ mod tests { ); } + #[test] + fn market_strict_runs_tag_their_turn_metadata() { + assert_eq!( + agent_run_metadata("app-1", "run-1", true), + serde_json::json!({ + "surface": MINIAPP_AGENT_SURFACE, + "appId": "app-1", + "runId": "run-1", + "acp_transport": true, + "marketStrict": true, + }) + ); + + let plan = + build_agent_submission_plan("app-1", "run-1", None, None, "/workspace", None, true); + assert!(plan.enable_tools); + assert_eq!(plan.metadata["marketStrict"], serde_json::json!(true)); + } + #[test] fn turn_text_starts_after_last_tool_result_for_requested_turn() { let messages = vec![ diff --git a/src/crates/execution/tool-contracts/src/framework.rs b/src/crates/execution/tool-contracts/src/framework.rs index b17c4933e9..943fe620ec 100644 --- a/src/crates/execution/tool-contracts/src/framework.rs +++ b/src/crates/execution/tool-contracts/src/framework.rs @@ -2229,6 +2229,7 @@ pub struct ToolRuntimeRestrictions { const MINIAPP_HEADLESS_AGENT_SURFACE: &str = "miniapp_agent"; const MINIAPP_HEADLESS_AGENT_OWNER_PREFIX: &str = "miniapp-agent:"; +const MINIAPP_MARKET_STRICT_METADATA_KEY: &str = "marketStrict"; /// MiniApp agent runs execute inside a MiniApp iframe without Flow Chat tool /// cards or AskUserQuestion UI. Treat those sessions as headless even on @@ -2247,6 +2248,18 @@ pub fn is_miniapp_headless_agent_run( created_by.is_some_and(|owner| owner.starts_with(MINIAPP_HEADLESS_AGENT_OWNER_PREFIX)) } +/// Marketplace MiniApps run under the strict runtime profile, so their agent +/// turns carry `marketStrict` in the submission metadata. Built-in and +/// locally authored MiniApps omit the flag and keep the compatibility tool set. +pub fn is_miniapp_market_strict_agent_run( + user_message_metadata: Option<&serde_json::Value>, +) -> bool { + user_message_metadata + .and_then(|metadata| metadata.get(MINIAPP_MARKET_STRICT_METADATA_KEY)) + .and_then(|value| value.as_bool()) + .unwrap_or(false) +} + pub fn miniapp_headless_agent_tool_restrictions() -> ToolRuntimeRestrictions { const DENIED_TOOLS: &[(&str, &str)] = &[ ( @@ -2301,6 +2314,49 @@ pub fn miniapp_headless_agent_tool_restrictions() -> ToolRuntimeRestrictions { } } +/// Tool set for a marketplace MiniApp agent turn. +/// +/// Marketplace MiniApps are third-party code, so their hidden agent sessions +/// must not reach the filesystem, the shell, or any host control surface. They +/// do need to answer questions about the live world, so the allowlist keeps +/// read-only web research and the clock that dates it. The deferred gateway pair +/// stays allowed because the execution gate matches the effective tool name, so +/// an allowlisted tool that resolves as deferred still has to pass this list. +/// An allowlist (rather than a longer deny list) keeps newly registered tools +/// closed by default. +pub fn miniapp_market_strict_agent_tool_restrictions() -> ToolRuntimeRestrictions { + const ALLOWED_TOOLS: &[&str] = &[ + "WebSearch", + "WebFetch", + "GetToolSpec", + "CallDeferredTool", + "GetTime", + ]; + + let mut restrictions = miniapp_headless_agent_tool_restrictions(); + restrictions.allowed_tool_names = ALLOWED_TOOLS + .iter() + .map(|name| (*name).to_string()) + .collect(); + restrictions +} + +/// Restrictions for one agent turn, keyed on whether it belongs to a MiniApp. +/// +/// Turns outside the MiniApp agent bridge keep the unrestricted default set. +pub fn miniapp_agent_run_tool_restrictions( + user_message_metadata: Option<&serde_json::Value>, + session_created_by: Option<&str>, +) -> ToolRuntimeRestrictions { + if !is_miniapp_headless_agent_run(user_message_metadata, session_created_by) { + return ToolRuntimeRestrictions::default(); + } + if is_miniapp_market_strict_agent_run(user_message_metadata) { + return miniapp_market_strict_agent_tool_restrictions(); + } + miniapp_headless_agent_tool_restrictions() +} + pub fn tool_restrictions_for_delegation_policy( delegation_policy: DelegationPolicy, ) -> ToolRuntimeRestrictions { @@ -2669,4 +2725,79 @@ mod tests { assert_eq!(registry.get_tool_names(), vec!["Read", "Write"]); } + + #[test] + fn market_strict_miniapp_runs_keep_web_research_and_drop_host_reach() { + let restrictions = miniapp_market_strict_agent_tool_restrictions(); + + assert!(restrictions.is_tool_allowed("WebSearch")); + assert!(restrictions.is_tool_allowed("WebFetch")); + assert!(restrictions.is_tool_allowed("GetToolSpec")); + + for denied in ["Read", "Write", "Edit", "ExecCommand", "Task", "Skill"] { + assert!( + !restrictions.is_tool_allowed(denied), + "{denied} must stay closed for marketplace MiniApp agent runs" + ); + } + + // The headless denials still apply on top of the allowlist. + assert!(!restrictions.is_tool_allowed("AskUserQuestion")); + assert!(matches!( + restrictions.ensure_tool_allowed("ComputerUse"), + Err(ToolRestrictionError::Denied { .. }) + )); + assert!(matches!( + restrictions.ensure_tool_allowed("Write"), + Err(ToolRestrictionError::NotAllowed { .. }) + )); + } + + #[test] + fn builtin_miniapp_runs_keep_the_compatibility_tool_set() { + let restrictions = miniapp_headless_agent_tool_restrictions(); + + assert!(restrictions.is_tool_allowed("WebSearch")); + assert!(restrictions.is_tool_allowed("WebFetch")); + assert!(restrictions.is_tool_allowed("Write")); + assert!(!restrictions.is_tool_allowed("AskUserQuestion")); + } + + #[test] + fn market_strict_detection_reads_the_turn_metadata_flag() { + assert!(is_miniapp_market_strict_agent_run(Some(&json!({ + "surface": "miniapp_agent", + "marketStrict": true, + })))); + assert!(!is_miniapp_market_strict_agent_run(Some(&json!({ + "surface": "miniapp_agent", + })))); + assert!(!is_miniapp_market_strict_agent_run(None)); + } + + #[test] + fn miniapp_run_restrictions_follow_the_runtime_profile_of_the_turn() { + let created_by = Some("miniapp-agent:app-1:run-1"); + let market_strict = json!({ + "surface": "miniapp_agent", + "marketStrict": true, + }); + let builtin = json!({ "surface": "miniapp_agent" }); + + assert!( + !miniapp_agent_run_tool_restrictions(Some(&market_strict), created_by) + .is_tool_allowed("Write") + ); + assert!( + miniapp_agent_run_tool_restrictions(Some(&builtin), created_by) + .is_tool_allowed("Write") + ); + // A turn outside the MiniApp bridge keeps the unrestricted default set, + // even when some other surface happens to carry the strict flag. + let other_surface = json!({ "surface": "chat", "marketStrict": true }); + assert!( + miniapp_agent_run_tool_restrictions(Some(&other_surface), None) + .is_tool_allowed("Write") + ); + } } diff --git a/src/crates/execution/tool-contracts/src/lib.rs b/src/crates/execution/tool-contracts/src/lib.rs index c95faa955c..397fd4aea7 100644 --- a/src/crates/execution/tool-contracts/src/lib.rs +++ b/src/crates/execution/tool-contracts/src/lib.rs @@ -59,37 +59,38 @@ pub use framework::{ collect_loaded_deferred_tool_specs, get_tool_spec_input_schema, get_tool_spec_is_concurrency_safe, get_tool_spec_is_readonly, get_tool_spec_short_description, is_bitfun_current_session_uri, is_bitfun_runtime_uri, is_bitfun_tool_uri, - is_miniapp_headless_agent_run, is_remote_posix_path_within_root, - is_tool_path_allowed_by_resolved_roots, materialize_static_tool_provider_groups, - miniapp_headless_agent_tool_restrictions, normalize_absolute_posix_path, normalize_host_path, - normalize_runtime_relative_path, parse_bitfun_current_session_uri, parse_bitfun_runtime_uri, - posix_resolve_path_with_workspace, posix_style_path_is_absolute, - render_get_tool_spec_tool_use_message, resolve_contextual_tool_manifest, - resolve_contextual_tool_manifest_from_provider, resolve_contextual_visible_tools, - resolve_contextual_visible_tools_from_provider, resolve_get_tool_spec_detail, - resolve_get_tool_spec_detail_from_provider, resolve_get_tool_spec_execution_plan, - resolve_get_tool_spec_execution_result_from_provider, resolve_host_path, - resolve_host_path_with_workspace, resolve_readonly_enabled_tools, resolve_tool_manifest_policy, - resolve_tool_path_with_context, resolve_tool_path_with_context_roots, - resolve_workspace_tool_path, sort_tool_manifest_definitions, - summarize_get_tool_spec_deferred_tools, tool_manifest_sort_rank, - tool_path_is_effectively_absolute, tool_restrictions_for_delegation_policy, - validate_deferred_tool_usage, validate_get_tool_spec_input, validate_tool_allowed_by_list, - ContextualToolManifest, ContextualToolManifestItem, ContextualVisibleTools, - DeferredToolUsageError, DynamicMcpToolInfo, DynamicToolInfo, GetToolSpecCatalogProvider, - GetToolSpecDeferredToolSummary, GetToolSpecDetail, GetToolSpecExecutionError, - GetToolSpecExecutionPlan, GetToolSpecLoadObservation, GetToolSpecRuntime, - LoadedDeferredToolSpec, ParsedBitFunCurrentSessionUri, ParsedBitFunRuntimeUri, - PortableToolContextProvider, PromptVisibleToolManifestItem, SnapshotToolDecorator, - SnapshotToolWrapper, SnapshotToolWrapperRef, StaticToolMaterializationError, - StaticToolProvider, StaticToolProviderFactory, StaticToolProviderGroup, StaticToolProviderPlan, - ToolCatalogRuntime, ToolCatalogSnapshotProvider, ToolContextFacts, ToolDecoratorRef, - ToolExecutionAccessError, ToolExposure, ToolManifestDefinition, ToolManifestPolicyResolution, - ToolManifestPolicyTool, ToolPathBackend, ToolPathContractError, ToolPathOperation, - ToolPathPolicy, ToolPathResolution, ToolRef, ToolRegistry, ToolRegistryItem, ToolRenderOptions, - ToolRestrictionError, ToolResult, ToolRuntimeAssembly, ToolRuntimeRestrictions, - ToolWorkspaceKind, ValidationResult, BITFUN_CURRENT_SESSION_URI_PREFIX, - BITFUN_RUNTIME_URI_PREFIX, GET_TOOL_SPEC_TOOL_NAME, + is_miniapp_headless_agent_run, is_miniapp_market_strict_agent_run, + is_remote_posix_path_within_root, is_tool_path_allowed_by_resolved_roots, + materialize_static_tool_provider_groups, miniapp_agent_run_tool_restrictions, + miniapp_headless_agent_tool_restrictions, miniapp_market_strict_agent_tool_restrictions, + normalize_absolute_posix_path, normalize_host_path, normalize_runtime_relative_path, + parse_bitfun_current_session_uri, parse_bitfun_runtime_uri, posix_resolve_path_with_workspace, + posix_style_path_is_absolute, render_get_tool_spec_tool_use_message, + resolve_contextual_tool_manifest, resolve_contextual_tool_manifest_from_provider, + resolve_contextual_visible_tools, resolve_contextual_visible_tools_from_provider, + resolve_get_tool_spec_detail, resolve_get_tool_spec_detail_from_provider, + resolve_get_tool_spec_execution_plan, resolve_get_tool_spec_execution_result_from_provider, + resolve_host_path, resolve_host_path_with_workspace, resolve_readonly_enabled_tools, + resolve_tool_manifest_policy, resolve_tool_path_with_context, + resolve_tool_path_with_context_roots, resolve_workspace_tool_path, + sort_tool_manifest_definitions, summarize_get_tool_spec_deferred_tools, + tool_manifest_sort_rank, tool_path_is_effectively_absolute, + tool_restrictions_for_delegation_policy, validate_deferred_tool_usage, + validate_get_tool_spec_input, validate_tool_allowed_by_list, ContextualToolManifest, + ContextualToolManifestItem, ContextualVisibleTools, DeferredToolUsageError, DynamicMcpToolInfo, + DynamicToolInfo, GetToolSpecCatalogProvider, GetToolSpecDeferredToolSummary, GetToolSpecDetail, + GetToolSpecExecutionError, GetToolSpecExecutionPlan, GetToolSpecLoadObservation, + GetToolSpecRuntime, LoadedDeferredToolSpec, ParsedBitFunCurrentSessionUri, + ParsedBitFunRuntimeUri, PortableToolContextProvider, PromptVisibleToolManifestItem, + SnapshotToolDecorator, SnapshotToolWrapper, SnapshotToolWrapperRef, + StaticToolMaterializationError, StaticToolProvider, StaticToolProviderFactory, + StaticToolProviderGroup, StaticToolProviderPlan, ToolCatalogRuntime, + ToolCatalogSnapshotProvider, ToolContextFacts, ToolDecoratorRef, ToolExecutionAccessError, + ToolExposure, ToolManifestDefinition, ToolManifestPolicyResolution, ToolManifestPolicyTool, + ToolPathBackend, ToolPathContractError, ToolPathOperation, ToolPathPolicy, ToolPathResolution, + ToolRef, ToolRegistry, ToolRegistryItem, ToolRenderOptions, ToolRestrictionError, ToolResult, + ToolRuntimeAssembly, ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, + BITFUN_CURRENT_SESSION_URI_PREFIX, BITFUN_RUNTIME_URI_PREFIX, GET_TOOL_SPEC_TOOL_NAME, }; pub use input_validator::InputValidator; pub use mcp_tool_bridge::{ diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.test.tsx b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.test.tsx index d9835a929d..25191c6c9a 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.test.tsx +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.test.tsx @@ -202,4 +202,26 @@ describe('useMiniAppBridge floating Agent routing', () => { expect(mocks.openMainSession).toHaveBeenCalledWith('session-1'); expect(mocks.agentRun).toHaveBeenCalledTimes(1); }); + + it('leaves the tool loop of a strict Agent run to the backend allowlist', async () => { + await act(async () => { + root.render(); + }); + const iframe = container.querySelector('iframe') as HTMLIFrameElement; + + await dispatchRpc(iframe, 1, 'agent.ensureSession', { + sessionName: 'Market Lens', + appDataWorkspace: 'chat', + }); + await dispatchRpc(iframe, 2, 'agent.run', { + sessionId: 'session-1', + prompt: 'Summarize the market', + }); + + // The host used to force enableTools=false for marketplace MiniApps, which + // also killed WebSearch/WebFetch. Tool access is now scoped by the backend + // research allowlist instead, so the bridge must not disable the loop. + expect(mocks.agentEnsureSession.mock.calls[0][1].enableTools).toBeUndefined(); + expect(mocks.agentRun.mock.calls[0][3].enableTools).toBeUndefined(); + }); }); diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts index 4fb0f71427..4c11a509fd 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts @@ -333,9 +333,7 @@ export function useMiniAppBridge( sessionId: params.sessionId as string | undefined, sessionName: params.sessionName as string | undefined, appDataWorkspace: String(params.appDataWorkspace ?? ''), - enableTools: strictRuntimeRef.current - ? false - : params.enableTools as boolean | undefined, + enableTools: params.enableTools as boolean | undefined, model: typeof params.model === 'string' ? params.model : undefined, }); agentSessionIdsRef.current.add(result.sessionId); @@ -414,9 +412,7 @@ export function useMiniAppBridge( sessionName: params.sessionName as string | undefined, displayText: typeof params.displayText === 'string' ? params.displayText : undefined, - enableTools: strictRuntimeRef.current - ? false - : params.enableTools as boolean | undefined, + enableTools: params.enableTools as boolean | undefined, sessionId: params.sessionId as string | undefined, appDataWorkspace: params.appDataWorkspace as string | undefined, model: typeof params.model === 'string' ? params.model : undefined,