autarkia 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
autarkia-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shihao Qi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.5
2
+ Name: autarkia
3
+ Version: 0.1.0
4
+ Summary: Tool governance for AI agents: registry, risk levels, role-based access, visibility rules, and execution guards. The tool layer of the Autarkia agent runtime.
5
+ Project-URL: Homepage, https://qishihao.icu/projects/autarkia
6
+ Project-URL: Repository, https://github.com/hakituo/autarkia-tools
7
+ Project-URL: Autarkia Runtime, https://github.com/hakituo/Autarkia
8
+ Project-URL: Author, https://qishihao.icu
9
+ Author-email: Shihao Qi <qishihao512@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,autarkia,function-calling,llm,role-based-access,self-hosted,tool-governance,tool-registry,tools
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: pydantic>=2.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # autarkia
30
+
31
+ **Tool governance for AI agents** — a registry, risk levels, role-based access, visibility rules, and execution guards for LLM tool calling.
32
+
33
+ This is the tool layer of [Autarkia](https://qishihao.icu/projects/autarkia), a self-hosted agent runtime that has run on a single machine for months: around 90 tools across 20+ domains, isolated per role, with pre-execution permission checks — not prompt promises.
34
+
35
+ ## Why
36
+
37
+ Most agent frameworks answer "how do I call a tool". This library answers the question that shows up after your agent has been running for a while: **who is allowed to call what, and what happens before it runs.**
38
+
39
+ - **Registry** — one place for tool instances, runtime enable/disable, and stable OpenAI function-calling schemas (byte-stable output so API-side prompt caches keep hitting).
40
+ - **Declared risk levels** — every tool carries an explicit risk class (`read`, `write`, `external_write`, `device_read`, `device_control`, `sensitive`). If you forgot to declare one, it simply does not appear in discovery. Risk is never inferred.
41
+ - **Role policies** — resident / on-demand / disabled per role, plus keyword routing for the first turn. A role that was never granted a tool cannot see it, search for it, or call it.
42
+ - **Visibility rules** — persona-level allow/deny by name or category, with mode and sensitive-mode overlays.
43
+ - **Execution guard** — authorization against the request's own snapshot, argument validation, and per-call context isolation (no shared mutable tool state across concurrent roles).
44
+
45
+ Zero runtime dependencies beyond `pydantic`. No framework lock-in: tools are plain classes, the registry is a plain object, and the OpenAI-compatible schema export works with any API that speaks function calling.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install autarkia
51
+ ```
52
+
53
+ ## Quick start
54
+
55
+ ```python
56
+ from pydantic import BaseModel
57
+
58
+ from autarkia.tools import BaseTool, ToolCatalog, ToolDeclaration, ToolRegistry
59
+
60
+
61
+ class EchoArgs(BaseModel):
62
+ text: str
63
+
64
+ class EchoTool(BaseTool):
65
+ name = "echo"
66
+ description = "Echo the given text back."
67
+ args_schema = EchoArgs
68
+
69
+ async def _run(self, text: str) -> str:
70
+ return text
71
+
72
+
73
+ # Declare what each tool is allowed to be. Undeclared tools stay invisible.
74
+ catalog = ToolCatalog()
75
+ catalog.declare(
76
+ "echo",
77
+ ToolDeclaration(
78
+ domain="utility",
79
+ risk_level="read",
80
+ load_policy="base",
81
+ tags=("demo", "echo"),
82
+ ),
83
+ )
84
+
85
+ registry = ToolRegistry(catalog=catalog)
86
+ registry.register(EchoTool())
87
+
88
+ # OpenAI function-calling format, cached and byte-stable.
89
+ print(registry.get_openai_tools())
90
+ ```
91
+
92
+ ### Role-based access
93
+
94
+ ```python
95
+ from autarkia.tools import RolePolicyResolver
96
+
97
+ profiles = {
98
+ "defaults": {"on_demand": ["domain:*"]},
99
+ "roles": {
100
+ "librarian": {"resident": ["echo"], "disabled": ["domain:execution"]},
101
+ },
102
+ "modes": {},
103
+ "routes": [
104
+ {"keywords": ["repeat"], "tools": ["echo"]},
105
+ ],
106
+ }
107
+
108
+ resolver = RolePolicyResolver(profiles, catalog=catalog)
109
+ policy = resolver.resolve(scope="librarian")
110
+ policy.allows("echo") # True
111
+ policy.allows("run_code") # False — disabled for this role
112
+
113
+ picked = resolver.select("please repeat this", scope="librarian")
114
+ ```
115
+
116
+ ### Execution guard
117
+
118
+ ```python
119
+ import asyncio
120
+ from autarkia.tools import execute_tool_call
121
+
122
+ result = asyncio.run(execute_tool_call(
123
+ agent=agent, # any object exposing .tool_registry
124
+ tool_name="echo",
125
+ arguments={"text": "hi"},
126
+ user_id="user-1",
127
+ allowed_tool_names=policy.visible_tool_names(),
128
+ ))
129
+ ```
130
+
131
+ The guard never trusts the model: it checks the tool against the allowed list passed by the orchestrator, validates arguments, and copies the tool instance so concurrent roles cannot bleed state into each other.
132
+
133
+ ## Concepts
134
+
135
+ | Piece | File | What it decides |
136
+ | --- | --- | --- |
137
+ | `BaseTool` | `autarkia.tools.base` | what a tool is (name, args schema, category) |
138
+ | `ToolCatalog` / `ToolDeclaration` | `autarkia.tools.metadata` | domain, tags, risk level, load policy — declared per tool |
139
+ | `ToolRegistry` | `autarkia.tools.registry` | registration, enable/disable, schema export, discovery search |
140
+ | `RoleToolPolicy` / `RolePolicyResolver` | `autarkia.tools.policy` | resident / on-demand / disabled per role, keyword routing |
141
+ | `filter_tool_names` | `autarkia.tools.visibility` | persona allow/deny, mode and sensitive-mode overlays |
142
+ | `execute_tool_call` | `autarkia.tools.execution` | the last line of defense before a tool runs |
143
+
144
+ Risk levels: `read` → `write` → `external_write` → `device_read` → `device_control` → `sensitive`.
145
+ Load policies: `base` (always offered), `on_demand` (found through discovery), `hidden` (exists, never advertised).
146
+
147
+ ## The bigger picture
148
+
149
+ Autarkia itself is not on PyPI — it is a single-machine runtime, not a SaaS. This package ships the governance layer that survived that experiment, because it turned out to be the part other projects kept asking for.
150
+
151
+ - Project page: <https://qishihao.icu/projects/autarkia>
152
+ - Runtime repository: <https://github.com/hakituo/Autarkia>
153
+ - This package: <https://github.com/hakituo/autarkia-tools>
154
+ - Author: Shihao Qi — <https://qishihao.icu>
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,130 @@
1
+ # autarkia
2
+
3
+ **Tool governance for AI agents** — a registry, risk levels, role-based access, visibility rules, and execution guards for LLM tool calling.
4
+
5
+ This is the tool layer of [Autarkia](https://qishihao.icu/projects/autarkia), a self-hosted agent runtime that has run on a single machine for months: around 90 tools across 20+ domains, isolated per role, with pre-execution permission checks — not prompt promises.
6
+
7
+ ## Why
8
+
9
+ Most agent frameworks answer "how do I call a tool". This library answers the question that shows up after your agent has been running for a while: **who is allowed to call what, and what happens before it runs.**
10
+
11
+ - **Registry** — one place for tool instances, runtime enable/disable, and stable OpenAI function-calling schemas (byte-stable output so API-side prompt caches keep hitting).
12
+ - **Declared risk levels** — every tool carries an explicit risk class (`read`, `write`, `external_write`, `device_read`, `device_control`, `sensitive`). If you forgot to declare one, it simply does not appear in discovery. Risk is never inferred.
13
+ - **Role policies** — resident / on-demand / disabled per role, plus keyword routing for the first turn. A role that was never granted a tool cannot see it, search for it, or call it.
14
+ - **Visibility rules** — persona-level allow/deny by name or category, with mode and sensitive-mode overlays.
15
+ - **Execution guard** — authorization against the request's own snapshot, argument validation, and per-call context isolation (no shared mutable tool state across concurrent roles).
16
+
17
+ Zero runtime dependencies beyond `pydantic`. No framework lock-in: tools are plain classes, the registry is a plain object, and the OpenAI-compatible schema export works with any API that speaks function calling.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install autarkia
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ from pydantic import BaseModel
29
+
30
+ from autarkia.tools import BaseTool, ToolCatalog, ToolDeclaration, ToolRegistry
31
+
32
+
33
+ class EchoArgs(BaseModel):
34
+ text: str
35
+
36
+ class EchoTool(BaseTool):
37
+ name = "echo"
38
+ description = "Echo the given text back."
39
+ args_schema = EchoArgs
40
+
41
+ async def _run(self, text: str) -> str:
42
+ return text
43
+
44
+
45
+ # Declare what each tool is allowed to be. Undeclared tools stay invisible.
46
+ catalog = ToolCatalog()
47
+ catalog.declare(
48
+ "echo",
49
+ ToolDeclaration(
50
+ domain="utility",
51
+ risk_level="read",
52
+ load_policy="base",
53
+ tags=("demo", "echo"),
54
+ ),
55
+ )
56
+
57
+ registry = ToolRegistry(catalog=catalog)
58
+ registry.register(EchoTool())
59
+
60
+ # OpenAI function-calling format, cached and byte-stable.
61
+ print(registry.get_openai_tools())
62
+ ```
63
+
64
+ ### Role-based access
65
+
66
+ ```python
67
+ from autarkia.tools import RolePolicyResolver
68
+
69
+ profiles = {
70
+ "defaults": {"on_demand": ["domain:*"]},
71
+ "roles": {
72
+ "librarian": {"resident": ["echo"], "disabled": ["domain:execution"]},
73
+ },
74
+ "modes": {},
75
+ "routes": [
76
+ {"keywords": ["repeat"], "tools": ["echo"]},
77
+ ],
78
+ }
79
+
80
+ resolver = RolePolicyResolver(profiles, catalog=catalog)
81
+ policy = resolver.resolve(scope="librarian")
82
+ policy.allows("echo") # True
83
+ policy.allows("run_code") # False — disabled for this role
84
+
85
+ picked = resolver.select("please repeat this", scope="librarian")
86
+ ```
87
+
88
+ ### Execution guard
89
+
90
+ ```python
91
+ import asyncio
92
+ from autarkia.tools import execute_tool_call
93
+
94
+ result = asyncio.run(execute_tool_call(
95
+ agent=agent, # any object exposing .tool_registry
96
+ tool_name="echo",
97
+ arguments={"text": "hi"},
98
+ user_id="user-1",
99
+ allowed_tool_names=policy.visible_tool_names(),
100
+ ))
101
+ ```
102
+
103
+ The guard never trusts the model: it checks the tool against the allowed list passed by the orchestrator, validates arguments, and copies the tool instance so concurrent roles cannot bleed state into each other.
104
+
105
+ ## Concepts
106
+
107
+ | Piece | File | What it decides |
108
+ | --- | --- | --- |
109
+ | `BaseTool` | `autarkia.tools.base` | what a tool is (name, args schema, category) |
110
+ | `ToolCatalog` / `ToolDeclaration` | `autarkia.tools.metadata` | domain, tags, risk level, load policy — declared per tool |
111
+ | `ToolRegistry` | `autarkia.tools.registry` | registration, enable/disable, schema export, discovery search |
112
+ | `RoleToolPolicy` / `RolePolicyResolver` | `autarkia.tools.policy` | resident / on-demand / disabled per role, keyword routing |
113
+ | `filter_tool_names` | `autarkia.tools.visibility` | persona allow/deny, mode and sensitive-mode overlays |
114
+ | `execute_tool_call` | `autarkia.tools.execution` | the last line of defense before a tool runs |
115
+
116
+ Risk levels: `read` → `write` → `external_write` → `device_read` → `device_control` → `sensitive`.
117
+ Load policies: `base` (always offered), `on_demand` (found through discovery), `hidden` (exists, never advertised).
118
+
119
+ ## The bigger picture
120
+
121
+ Autarkia itself is not on PyPI — it is a single-machine runtime, not a SaaS. This package ships the governance layer that survived that experiment, because it turned out to be the part other projects kept asking for.
122
+
123
+ - Project page: <https://qishihao.icu/projects/autarkia>
124
+ - Runtime repository: <https://github.com/hakituo/Autarkia>
125
+ - This package: <https://github.com/hakituo/autarkia-tools>
126
+ - Author: Shihao Qi — <https://qishihao.icu>
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "autarkia"
7
+ version = "0.1.0"
8
+ description = "Tool governance for AI agents: registry, risk levels, role-based access, visibility rules, and execution guards. The tool layer of the Autarkia agent runtime."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Shihao Qi", email = "qishihao512@gmail.com" },
14
+ ]
15
+ keywords = [
16
+ "agent",
17
+ "tools",
18
+ "tool-registry",
19
+ "tool-governance",
20
+ "llm",
21
+ "function-calling",
22
+ "role-based-access",
23
+ "autarkia",
24
+ "self-hosted",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = [
41
+ "pydantic>=2.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://qishihao.icu/projects/autarkia"
46
+ Repository = "https://github.com/hakituo/autarkia-tools"
47
+ "Autarkia Runtime" = "https://github.com/hakituo/Autarkia"
48
+ Author = "https://qishihao.icu"
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/autarkia"]
55
+
56
+ [tool.hatch.build.targets.sdist]
57
+ include = ["src", "tests", "README.md", "LICENSE"]
@@ -0,0 +1,35 @@
1
+ """Autarkia — tool governance for AI agents.
2
+
3
+ Registry, risk levels, role-based access, visibility rules, and execution
4
+ guards for LLM tool calling. The tool layer of the Autarkia agent runtime.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from autarkia.tools import (
10
+ BaseTool,
11
+ RolePolicyResolver,
12
+ RoleToolPolicy,
13
+ ToolCatalog,
14
+ ToolDeclaration,
15
+ ToolDiscoveryMetadata,
16
+ ToolRegistry,
17
+ ToolVisibilityContext,
18
+ ToolVisibilityRules,
19
+ execute_tool_call,
20
+ filter_tool_names,
21
+ )
22
+
23
+ __all__ = [
24
+ "BaseTool",
25
+ "RolePolicyResolver",
26
+ "RoleToolPolicy",
27
+ "ToolCatalog",
28
+ "ToolDeclaration",
29
+ "ToolDiscoveryMetadata",
30
+ "ToolRegistry",
31
+ "ToolVisibilityContext",
32
+ "ToolVisibilityRules",
33
+ "execute_tool_call",
34
+ "filter_tool_names",
35
+ ]
File without changes
@@ -0,0 +1,32 @@
1
+ """Tool governance: registry, declared risk levels, role policies, visibility, execution guard."""
2
+
3
+ from autarkia.tools.base import BaseTool
4
+ from autarkia.tools.execution import execute_tool_call
5
+ from autarkia.tools.metadata import (
6
+ ToolCatalog,
7
+ ToolDeclaration,
8
+ ToolDiscoveryMetadata,
9
+ search_tool_metadata,
10
+ )
11
+ from autarkia.tools.policy import RolePolicyResolver, RoleToolPolicy
12
+ from autarkia.tools.registry import ToolRegistry
13
+ from autarkia.tools.visibility import (
14
+ ToolVisibilityContext,
15
+ ToolVisibilityRules,
16
+ filter_tool_names,
17
+ )
18
+
19
+ __all__ = [
20
+ "BaseTool",
21
+ "RolePolicyResolver",
22
+ "RoleToolPolicy",
23
+ "ToolCatalog",
24
+ "ToolDeclaration",
25
+ "ToolDiscoveryMetadata",
26
+ "ToolRegistry",
27
+ "ToolVisibilityContext",
28
+ "ToolVisibilityRules",
29
+ "execute_tool_call",
30
+ "filter_tool_names",
31
+ "search_tool_metadata",
32
+ ]
@@ -0,0 +1,86 @@
1
+ """BaseTool — what a tool is.
2
+
3
+ Ported from the Autarkia runtime's tool base class. Host-specific helpers
4
+ (QQ master detection, persona scope resolution) were replaced with
5
+ runtime-context conventions so the base class stays dependency-free.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, Dict, Optional, Type
10
+
11
+ from pydantic import BaseModel
12
+
13
+
14
+ class BaseTool(ABC):
15
+ """Base class for all tools.
16
+
17
+ Class attributes:
18
+ name: unique tool name (used as the registry key).
19
+ description: full description, also used as the function-calling
20
+ description in exported schemas.
21
+ args_schema: optional pydantic model describing the arguments.
22
+ short_description: one-line hint for prompt injection; overrides
23
+ ``description`` when a compact list is rendered.
24
+ category: free-form grouping label (e.g. "memory", "device").
25
+ enabled_by_default: whether the tool is offered before any
26
+ explicit enable/disable call.
27
+ """
28
+
29
+ name: str
30
+ description: str
31
+ args_schema: Optional[Type[BaseModel]] = None
32
+ short_description: Optional[str] = None
33
+ category: str = "utility"
34
+ enabled_by_default: bool = True
35
+
36
+ def set_runtime_context(self, context: Dict[str, Any]) -> None:
37
+ """Inject runtime context (agent, user_id, scope, ...) before execution."""
38
+ self._runtime_context = context
39
+
40
+ def _get_ctx(self, key: str, default: Any = None) -> Any:
41
+ """Convenience accessor for runtime context keys."""
42
+ return getattr(self, "_runtime_context", {}).get(key, default)
43
+
44
+ def is_master(self) -> bool:
45
+ """Whether the current session belongs to a master/admin caller.
46
+
47
+ The orchestrator decides what "master" means and injects either
48
+ ``is_master: bool`` or ``master_ids: collection[str]`` alongside
49
+ ``user_id`` into the runtime context.
50
+ """
51
+ if bool(self._get_ctx("is_master", False)):
52
+ return True
53
+ master_ids = self._get_ctx("master_ids")
54
+ if not master_ids:
55
+ return False
56
+ user_id = str(self._get_ctx("user_id") or "").strip()
57
+ return bool(user_id) and user_id in {str(item) for item in master_ids}
58
+
59
+ def current_scope(self) -> str:
60
+ """Scope of the current call (role / persona slug / tenant id).
61
+
62
+ Resolution order, all injected per request by the orchestrator:
63
+ ``scope`` → ``persona_filename`` → empty string. Never reads
64
+ global singletons, so concurrent roles stay isolated.
65
+ """
66
+ scope = str(self._get_ctx("scope") or "").strip()
67
+ if scope:
68
+ return scope
69
+ return str(self._get_ctx("persona_filename") or "").strip()
70
+
71
+ @abstractmethod
72
+ async def _run(self, *args, **kwargs) -> Any:
73
+ """Implementation of the tool."""
74
+
75
+ async def run(self, *args, **kwargs) -> str:
76
+ """Execute the tool and return the result as a string.
77
+
78
+ Tool-internal exceptions are converted into an error string on
79
+ purpose: the model reads the result either way, and a weather API
80
+ timeout should look like a retryable outcome, not a crash.
81
+ """
82
+ try:
83
+ result = await self._run(*args, **kwargs)
84
+ return str(result)
85
+ except Exception as exc: # noqa: BLE001 - deliberate boundary
86
+ return f"Error executing tool {self.name}: {exc}"
@@ -0,0 +1,61 @@
1
+ """Unified execution boundary: authorization snapshot + per-call isolation.
2
+
3
+ Ported from the Autarkia runtime unchanged in behavior — it is already
4
+ host-free. This is the last line of defense before a tool runs.
5
+ """
6
+
7
+ from copy import copy
8
+ import json
9
+ from typing import Any, List, Mapping, Optional
10
+
11
+
12
+ async def execute_tool_call(
13
+ *,
14
+ agent: Any,
15
+ tool_name: str,
16
+ arguments: Mapping[str, Any],
17
+ user_id: str,
18
+ allowed_tool_names: List[str],
19
+ persona_filename: Optional[str] = None,
20
+ is_sensitive_mode: bool = False,
21
+ used_tool_names: Optional[List[str]] = None,
22
+ last_user_text: str = "",
23
+ ) -> str:
24
+ """Execute one tool call against the request's own authorization snapshot.
25
+
26
+ The model never picks roles, paths, or permissions here — everything
27
+ comes from the orchestrator's snapshot for this request.
28
+
29
+ ``used_tool_names`` collects what was authorized *and* actually
30
+ dispatched (not what succeeded — ``BaseTool.run()`` turns tool errors
31
+ into strings by design, and a timed-out weather call is exactly the
32
+ thing the next turn should be allowed to retry).
33
+
34
+ ``last_user_text`` carries the raw user message for tools that must
35
+ only reference what the user actually said.
36
+ """
37
+ registry = getattr(agent, "tool_registry", None)
38
+ if tool_name not in allowed_tool_names or registry is None or not registry.is_enabled(tool_name):
39
+ return json.dumps(
40
+ {"ok": False, "error": "tool not allowed for the current role", "tool": tool_name},
41
+ ensure_ascii=False,
42
+ )
43
+ if not isinstance(arguments, Mapping):
44
+ return json.dumps(
45
+ {"ok": False, "error": "tool arguments must be an object", "tool": tool_name},
46
+ ensure_ascii=False,
47
+ )
48
+ # The registry stores reusable implementations; every call gets an
49
+ # isolated copy so concurrent roles cannot bleed state into each other.
50
+ tool = copy(registry.get_tool(tool_name))
51
+ tool.set_runtime_context({
52
+ "agent": agent,
53
+ "user_id": user_id,
54
+ "persona_filename": persona_filename,
55
+ "scope": "sensitive" if is_sensitive_mode else "sfw",
56
+ "allowed_tool_names": list(allowed_tool_names),
57
+ "last_user_text": str(last_user_text or ""),
58
+ })
59
+ if used_tool_names is not None and tool_name not in used_tool_names:
60
+ used_tool_names.append(tool_name)
61
+ return await tool.run(**dict(arguments))