diff --git a/docs/en/Best Practice/functionCall.md b/docs/en/Best Practice/functionCall.md index bde3f7e40..f49c0eafb 100644 --- a/docs/en/Best Practice/functionCall.md +++ b/docs/en/Best Practice/functionCall.md @@ -284,7 +284,7 @@ prepared = manager.prepare_tool_calls(calls, authorization_policy=lambda call: A result = manager.execute_prepared(prepared, selected_indices=(0,), approved_indices=(0,)) ``` -Unapproved ASK calls cannot execute. Selection is not approval. Decisions are fixed during preparation; failed preparation never executes a tool. `require_host_file_access=True` remains an explicit declaration-completeness check. +Unapproved ASK calls cannot execute. Selection is not approval. Decisions are fixed during preparation; failed preparation never executes a tool. Complete code is as follows: ```python diff --git a/docs/zh/Best Practice/functionCall.md b/docs/zh/Best Practice/functionCall.md index a5dd31e82..21f676930 100644 --- a/docs/zh/Best Practice/functionCall.md +++ b/docs/zh/Best Practice/functionCall.md @@ -328,7 +328,7 @@ prepared = manager.prepare_tool_calls(calls, authorization_policy=lambda call: A result = manager.execute_prepared(prepared, selected_indices=(0,), approved_indices=(0,)) ``` -选择调用不代表批准。未批准的 ASK 不能执行,准备失败保留原错误。授权决定在 prepare 固定。`require_host_file_access=True` 仍用于显式声明完整性检查。 +选择调用不代表批准。未批准的 ASK 不能执行,准备失败保留原错误。授权决定在 prepare 固定。 完整代码如下: ```python diff --git a/lazyllm/docs/tools/tool_agent.py b/lazyllm/docs/tools/tool_agent.py index e83b8d1c8..2e5e81a58 100644 --- a/lazyllm/docs/tools/tool_agent.py +++ b/lazyllm/docs/tools/tool_agent.py @@ -264,7 +264,6 @@ 复用此批次。调用方完成整个批次的权限判断后,调用 execute_prepared 执行获准项。 working_directory 可为 host_file resolver 提供请求级绝对工作目录,不改变进程 cwd。 authorization_policy=None 时准备成功即 ALLOW;调用方可传入自定义策略,LazyLLM 不提供内置策略。决策在 prepare 固定。 -require_host_file_access=True 会检查本轮 exposed tools,存在 UNDECLARED 则拒绝准备,适合注册契约测试。 传入 allowed_tool_names 可限制本轮可见工具。文件 resolver 失败产生 PREPARATION_FAILED,不会降级为可执行调用。 ''') @@ -275,9 +274,7 @@ working_directory optionally supplies an absolute request-local base to host_file resolvers; it never changes process cwd. With authorization_policy=None, every ready call is ALLOW. Callers may supply a custom policy; LazyLLM has no built-in policy. Decisions are fixed during preparation. -Set require_host_file_access=True to reject UNDECLARED tools in the exposed set (optionally limited by -allowed_tool_names). This also supports registration contract tests with an empty call list. File-resolution -errors produce PREPARATION_FAILED calls; they never fall back to executable exclusive calls. +File-resolution errors produce PREPARATION_FAILED calls; they never fall back to executable exclusive calls. ''') add_chinese_doc('ToolManager.execute_prepared', '''\ @@ -376,8 +373,7 @@ ... return Path(path).read_text() >>> manager = ToolManager([read_text]) >>> prepared = manager.prepare_tool_calls( -... {'function': {'name': 'read_text', 'arguments': {'path': 'notes.txt'}}}, -... require_host_file_access=True) +... {'function': {'name': 'read_text', 'arguments': {'path': 'notes.txt'}}}) >>> # Inspect all calls, wait for application approvals, and then execute the same batch. >>> # This read-only call is ALLOW without an application policy. >>> result = manager.execute_prepared(prepared) diff --git a/lazyllm/tools/agent/toolsManager.py b/lazyllm/tools/agent/toolsManager.py index d59e23616..170aed5e7 100644 --- a/lazyllm/tools/agent/toolsManager.py +++ b/lazyllm/tools/agent/toolsManager.py @@ -1205,19 +1205,8 @@ def _execute_tool_calls(cls, count, callables, call_arguments, accesses): raise min(errors, key=lambda item: item[0])[1] return lazyllm.package(ordered_results) - def prepare_tool_calls(self, tools, allowed_tool_names=None, *, - require_host_file_access=False, working_directory=None, - require_host_file=None, + def prepare_tool_calls(self, tools, allowed_tool_names=None, *, working_directory=None, authorization_policy=None): - if require_host_file is not None: - require_host_file_access = require_host_file - if require_host_file_access: - exposed = self._tool_call if allowed_tool_names is None else allowed_tool_names - undeclared = sorted(name for name in exposed if name in self._tool_call - and self._tool_call[name].runtime_metadata.host_file_access - is HostFileAccess.UNDECLARED) - if undeclared: - raise ValueError(f'Tools have undeclared host file access: {undeclared}') if working_directory is not None and not os.path.isabs(working_directory): raise ValueError('working_directory must be absolute') token = _HOST_WORKING_DIRECTORY.set(working_directory) @@ -1256,10 +1245,6 @@ def execute_prepared(self, prepared, *, approved_indices=(), selected_indices=No if item.authorization is AuthorizationDecision.ALLOW) + approved else: selected = tuple(selected_indices) - if any(type(index) is not int or index < 0 or index >= len(prepared) for index in selected): - raise IndexError('selected prepared-call index is out of range') - if len(set(selected)) != len(selected): - raise ValueError('selected prepared-call indices must be unique') return self._execute_prepared_batch( prepared, selected, include_skipped=True, execution_context=execution_context, approved_indices=approved) @@ -1326,14 +1311,6 @@ def execute_with_records(self, tools: Union[Dict[str, Any], List[Dict[str, Any]] else: snapshots = tuple(copy.deepcopy(item.prepared) for item in prepared._invocations) selected = tuple(dispatch_selector(snapshots)) if len(prepared) else () - if any(type(index) is not int or index < 0 or index >= len(prepared) for index in selected): - raise IndexError('selected prepared-call index is out of range') - if len(set(selected)) != len(selected): - raise ValueError('selected prepared-call indices must be unique') - denied = [index for index in selected - if prepared[index].ready and prepared[index].authorization is AuthorizationDecision.DENY] - if denied: - raise ValueError('DENY prepared calls cannot be selected for execution') batch = self._execute_prepared_batch(prepared, selected, include_skipped=False) return replace(batch, duration_ms=round(max(0.0, (time.monotonic() - started) * 1000.0))) diff --git a/tests/basic_tests/Tools/test_host_file_lifecycle.py b/tests/basic_tests/Tools/test_host_file_lifecycle.py index 657f0bfb9..cb660affa 100644 --- a/tests/basic_tests/Tools/test_host_file_lifecycle.py +++ b/tests/basic_tests/Tools/test_host_file_lifecycle.py @@ -90,7 +90,7 @@ def tool(path: str, count: int): manager = ToolManager([tool]) original = call(path='result.txt', count='2') - prepared = manager.prepare_tool_calls(original, require_host_file_access=True) + prepared = manager.prepare_tool_calls(original) assert events == [('resolve', 2)] and not target.exists() assert prepared[0].host_file_access is HostFileAccess.DECLARED assert prepared[0].host_files == (HostFileIntent(str(target), 'write'),) @@ -153,22 +153,6 @@ def tool(count: int): assert not prepared[0].ready and resolved == [] -def test_strict_preparation_checks_all_exposed_tools(): - def tool(value: str): - '''Return a value. - - Args: - value: Input value. - ''' - return value - - manager = ToolManager([tool]) - with pytest.raises(ValueError, match='tool'): - manager.prepare_tool_calls([], require_host_file_access=True) - assert manager.execute_with_records(call(value='legacy')).results[0] == {'ok': True, 'value': 'legacy'} - assert len(manager.prepare_tool_calls([], allowed_tool_names=set(), require_host_file_access=True)) == 0 - - def test_nested_views_are_read_only_and_results_do_not_mutate_batch(): @fc_register(host_file='NONE') def tool(data: dict): @@ -310,7 +294,6 @@ def test_skill_tools_have_explicit_capabilities(tmp_path): 'read_reference': HostFileAccess.NONE, 'run_script': HostFileAccess.OPAQUE, } - manager.prepare_tool_calls([], require_host_file_access=True) def test_validation_cannot_change_a_resolver_approved_path(tmp_path): @@ -452,7 +435,7 @@ def test_builtin_paths_use_request_working_directory(tmp_path): manager = ToolManager([read, write, remove, shell, todo_write]) batch = manager.prepare_tool_calls( - call('read', path='notes.txt'), require_host_file_access=True, working_directory=str(tmp_path)) + call('read', path='notes.txt'), working_directory=str(tmp_path)) assert batch[0].validated_arguments['path'] == str(tmp_path / 'notes.txt') assert batch[0].host_files == (HostFileIntent(str(tmp_path / 'notes.txt'), 'read'),)