Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/en/Best Practice/functionCall.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/Best Practice/functionCall.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions lazyllm/docs/tools/tool_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,不会降级为可执行调用。
''')

Expand All @@ -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', '''\
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 1 addition & 24 deletions lazyllm/tools/agent/toolsManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)))

Expand Down
21 changes: 2 additions & 19 deletions tests/basic_tests/Tools/test_host_file_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'),)

Expand Down
Loading