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.
- autarkia-0.1.0/.gitignore +11 -0
- autarkia-0.1.0/LICENSE +21 -0
- autarkia-0.1.0/PKG-INFO +158 -0
- autarkia-0.1.0/README.md +130 -0
- autarkia-0.1.0/pyproject.toml +57 -0
- autarkia-0.1.0/src/autarkia/__init__.py +35 -0
- autarkia-0.1.0/src/autarkia/py.typed +0 -0
- autarkia-0.1.0/src/autarkia/tools/__init__.py +32 -0
- autarkia-0.1.0/src/autarkia/tools/base.py +86 -0
- autarkia-0.1.0/src/autarkia/tools/execution.py +61 -0
- autarkia-0.1.0/src/autarkia/tools/metadata.py +197 -0
- autarkia-0.1.0/src/autarkia/tools/policy.py +131 -0
- autarkia-0.1.0/src/autarkia/tools/registry.py +271 -0
- autarkia-0.1.0/src/autarkia/tools/visibility.py +174 -0
- autarkia-0.1.0/tests/__init__.py +0 -0
- autarkia-0.1.0/tests/conftest.py +79 -0
- autarkia-0.1.0/tests/test_base.py +63 -0
- autarkia-0.1.0/tests/test_execution.py +105 -0
- autarkia-0.1.0/tests/test_metadata.py +100 -0
- autarkia-0.1.0/tests/test_policy.py +114 -0
- autarkia-0.1.0/tests/test_registry.py +144 -0
- autarkia-0.1.0/tests/test_visibility.py +107 -0
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.
|
autarkia-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
autarkia-0.1.0/README.md
ADDED
|
@@ -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))
|