vectros-sdk 2.0.0__py3-none-any.whl
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.
- vectros/__init__.py +24 -0
- vectros/__main__.py +3 -0
- vectros/_kernel.py +441 -0
- vectros/_tracing.py +257 -0
- vectros/agent.py +440 -0
- vectros/cli.py +290 -0
- vectros/errors.py +30 -0
- vectros/tools.py +130 -0
- vectros_sdk-2.0.0.dist-info/METADATA +213 -0
- vectros_sdk-2.0.0.dist-info/RECORD +14 -0
- vectros_sdk-2.0.0.dist-info/WHEEL +5 -0
- vectros_sdk-2.0.0.dist-info/entry_points.txt +2 -0
- vectros_sdk-2.0.0.dist-info/licenses/LICENSE +21 -0
- vectros_sdk-2.0.0.dist-info/top_level.txt +1 -0
vectros/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Vectros SDK: build AI agents that run on the AIOS kernel.
|
|
2
|
+
|
|
3
|
+
from vectros import Agent, tool
|
|
4
|
+
|
|
5
|
+
@tool
|
|
6
|
+
def weather(city: str) -> str:
|
|
7
|
+
'''Current weather for a city.'''
|
|
8
|
+
return "31 C, clear"
|
|
9
|
+
|
|
10
|
+
agent = Agent("helper", tools=[weather])
|
|
11
|
+
print(agent.run("weather in Pune?"))
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .agent import Agent, Event, Storage, ask_terminal
|
|
15
|
+
from .errors import (KernelError, KernelUnavailable, StepLimitReached, ToolDenied,
|
|
16
|
+
VectrosError)
|
|
17
|
+
from .tools import KernelTool, Tool, tool
|
|
18
|
+
|
|
19
|
+
__version__ = "2.0.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Agent", "Event", "KernelError", "KernelTool", "KernelUnavailable", "StepLimitReached",
|
|
23
|
+
"Storage", "Tool", "ToolDenied", "VectrosError", "ask_terminal", "tool",
|
|
24
|
+
]
|
vectros/__main__.py
ADDED
vectros/_kernel.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""ctypes binding to libaios.so, the userspace half of the AIOS kernel ABI.
|
|
2
|
+
|
|
3
|
+
Everything here talks to /dev/aios. The higher layers (Agent, tool) only use
|
|
4
|
+
the methods of :class:`Kernel`, so tests can swap in a fake with the same
|
|
5
|
+
surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
import ctypes.util
|
|
12
|
+
import errno
|
|
13
|
+
import fcntl
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import struct
|
|
17
|
+
import time
|
|
18
|
+
import warnings
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Callable, Generator
|
|
21
|
+
|
|
22
|
+
from ._tracing import AIOSTraceContextC, TraceSpan
|
|
23
|
+
from .errors import KernelError, KernelUnavailable, ToolDenied, VectrosError
|
|
24
|
+
|
|
25
|
+
ABI_VERSION = 4
|
|
26
|
+
|
|
27
|
+
SYSCALL_LLM, SYSCALL_MEMORY, SYSCALL_STORAGE, SYSCALL_TOOL = 0, 1, 2, 3
|
|
28
|
+
|
|
29
|
+
STATE_CREATED, STATE_QUEUED, STATE_PROCESSING, STATE_DONE = 0, 1, 2, 3
|
|
30
|
+
STATE_FAILED, STATE_CANCELLED = 4, 5
|
|
31
|
+
STATE_HELD_APPROVAL = 9
|
|
32
|
+
TERMINAL_STATES = (STATE_DONE, STATE_FAILED, STATE_CANCELLED)
|
|
33
|
+
|
|
34
|
+
STO_WRITE, STO_RETRIEVE = 3, 4
|
|
35
|
+
|
|
36
|
+
OWNER_APPROVE_TOOL, OWNER_DENY_TOOL = 3, 4
|
|
37
|
+
_IOCTL_OWNER_ACTION = 0x40A04173 # _IOW('A', 0x73, struct aios_owner_action)
|
|
38
|
+
_IOCTL_GET_ABI_INFO = 0x800841FD # _IOR('A', 0xFD, struct aios_abi_info)
|
|
39
|
+
|
|
40
|
+
# ABI 4.3
|
|
41
|
+
AGENT_REGISTER_STABLE = 1
|
|
42
|
+
TOOL_CALL_CLIENT, TOOL_CALL_APPROVAL = 1, 2
|
|
43
|
+
CLIENT_RESULT_MAX = 64 * 1024
|
|
44
|
+
_IOCTL_LIST_TOOLS_NR = 0x54 # _IOWR('A', 0x54, ...), size computed below
|
|
45
|
+
|
|
46
|
+
_TOOL_HEADER = struct.Struct("@HHIQ64sII")
|
|
47
|
+
_STORAGE_HEADER = struct.Struct("@I256sQI4xQQ")
|
|
48
|
+
_TOOL_ENTRY = struct.Struct("@HHI64s32s256sIII")
|
|
49
|
+
_TOOL_PAGE_HEADER = struct.Struct("@HHIIIII")
|
|
50
|
+
|
|
51
|
+
RESULT_MAX = 256 * 1024
|
|
52
|
+
|
|
53
|
+
_LIB_CANDIDATES = (
|
|
54
|
+
"/usr/lib/libaios.so",
|
|
55
|
+
"/usr/lib64/libaios.so",
|
|
56
|
+
"/usr/local/lib/libaios.so",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_library() -> str:
|
|
61
|
+
"""Locate libaios.so: $VECTROS_LIBAIOS, system paths, then the linker."""
|
|
62
|
+
explicit = os.environ.get("VECTROS_LIBAIOS") or os.environ.get("AIOS_LIB")
|
|
63
|
+
if explicit:
|
|
64
|
+
return explicit
|
|
65
|
+
for candidate in _LIB_CANDIDATES:
|
|
66
|
+
if Path(candidate).exists():
|
|
67
|
+
return candidate
|
|
68
|
+
found = ctypes.util.find_library("aios")
|
|
69
|
+
if found:
|
|
70
|
+
return found
|
|
71
|
+
raise KernelUnavailable(
|
|
72
|
+
"libaios.so not found; install the aios package or set VECTROS_LIBAIOS")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _tool_payload(name: str, args: dict, flags: int) -> bytes:
|
|
76
|
+
raw_name = name.encode()
|
|
77
|
+
if not raw_name or len(raw_name) >= 64 or b"\0" in raw_name:
|
|
78
|
+
raise ValueError("tool name must be 1-63 UTF-8 bytes without NUL")
|
|
79
|
+
raw_args = json.dumps(args, separators=(",", ":")).encode()
|
|
80
|
+
return _TOOL_HEADER.pack(ABI_VERSION, _TOOL_HEADER.size, flags, 0,
|
|
81
|
+
raw_name.ljust(64, b"\0"), len(raw_args), 0) + raw_args
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _errno_error(what: str) -> KernelError:
|
|
85
|
+
code = ctypes.get_errno()
|
|
86
|
+
return KernelError(code, f"{what}: {os.strerror(code)}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Kernel:
|
|
90
|
+
"""One open handle on /dev/aios."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, lib_path: str | None = None):
|
|
93
|
+
path = lib_path or find_library()
|
|
94
|
+
try:
|
|
95
|
+
self.lib = ctypes.CDLL(path, use_errno=True)
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise KernelUnavailable(f"cannot load {path}: {exc}") from exc
|
|
98
|
+
self._bind()
|
|
99
|
+
self.h = self.lib.aios_open()
|
|
100
|
+
if not self.h:
|
|
101
|
+
code = ctypes.get_errno()
|
|
102
|
+
hints = {
|
|
103
|
+
errno.ENOENT: "aios.ko is not loaded",
|
|
104
|
+
errno.EACCES: "add your user to group aios and log in again",
|
|
105
|
+
}
|
|
106
|
+
detail = hints.get(code, os.strerror(code) if code else "unknown error")
|
|
107
|
+
raise KernelUnavailable(f"cannot open /dev/aios: {detail}")
|
|
108
|
+
# Per syscall: buffers the kernel reads after submit, and the trace
|
|
109
|
+
# span to finish when the syscall is waited for.
|
|
110
|
+
self._inflight: dict[int, tuple[object, TraceSpan | None]] = {}
|
|
111
|
+
self.abi_minor = self._abi_minor()
|
|
112
|
+
missing = [name for name in ("aios_register_agent_ex", "aios_client_tool_complete")
|
|
113
|
+
if not hasattr(self.lib, name)]
|
|
114
|
+
if self.abi_minor >= 3 and missing:
|
|
115
|
+
warnings.warn(
|
|
116
|
+
f"{path} predates the loaded kernel (ABI 4.{self.abi_minor}): missing "
|
|
117
|
+
f"{', '.join(missing)}. Agent IDs are random and @tool calls bypass the "
|
|
118
|
+
"kernel. Install the current libaios.so or set VECTROS_LIBAIOS.",
|
|
119
|
+
RuntimeWarning, stacklevel=3)
|
|
120
|
+
|
|
121
|
+
def _abi_minor(self) -> int:
|
|
122
|
+
info = bytearray(8)
|
|
123
|
+
try:
|
|
124
|
+
fcntl.ioctl(self.fd, _IOCTL_GET_ABI_INFO, info, True)
|
|
125
|
+
except OSError:
|
|
126
|
+
return 0
|
|
127
|
+
major, minor, _ = struct.unpack("@HHI", info)
|
|
128
|
+
return minor if major == ABI_VERSION else 0
|
|
129
|
+
|
|
130
|
+
def _bind(self) -> None:
|
|
131
|
+
void_p, u64, u32 = ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint32
|
|
132
|
+
int_p = ctypes.POINTER(ctypes.c_int)
|
|
133
|
+
signatures = {
|
|
134
|
+
"aios_register_agent": [void_p, ctypes.c_char_p, ctypes.POINTER(u64)],
|
|
135
|
+
"aios_register_agent_ex": [void_p, ctypes.c_char_p, u32, ctypes.POINTER(u64)],
|
|
136
|
+
"aios_client_tool_complete": [void_p, u64, void_p, u32, ctypes.c_int],
|
|
137
|
+
"aios_unregister_agent": [void_p, u64],
|
|
138
|
+
"aios_setup_core": [void_p, u64, u32, u32, u32, ctypes.c_char_p],
|
|
139
|
+
"aios_submit": [void_p, u64, u64, u32, u32, u32, void_p, u32, u64,
|
|
140
|
+
ctypes.POINTER(u64)],
|
|
141
|
+
"aios_submit_trace": [void_p, u64, u64, u32, u32, u32, void_p, u32, u64,
|
|
142
|
+
ctypes.POINTER(AIOSTraceContextC), ctypes.POINTER(u64)],
|
|
143
|
+
"aios_wait": [void_p, u64, void_p, ctypes.POINTER(u32), int_p, int_p],
|
|
144
|
+
"aios_query_status": [void_p, u64, int_p, int_p],
|
|
145
|
+
"aios_read_stream": [void_p, u64, void_p, ctypes.POINTER(u32), ctypes.c_int,
|
|
146
|
+
int_p, int_p],
|
|
147
|
+
"aios_cancel": [void_p, u64],
|
|
148
|
+
"aios_trace_get_enabled": [void_p, u64, int_p],
|
|
149
|
+
}
|
|
150
|
+
self.lib.aios_open.restype = void_p
|
|
151
|
+
self.lib.aios_close.argtypes = [void_p]
|
|
152
|
+
self.lib.aios_close.restype = None
|
|
153
|
+
for name, argtypes in signatures.items():
|
|
154
|
+
function = getattr(self.lib, name, None)
|
|
155
|
+
if function is not None:
|
|
156
|
+
function.argtypes = argtypes
|
|
157
|
+
function.restype = ctypes.c_int
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def fd(self) -> int:
|
|
161
|
+
# struct aios_handle is opaque; its first member is the /dev/aios fd.
|
|
162
|
+
return ctypes.cast(self.h, ctypes.POINTER(ctypes.c_int))[0]
|
|
163
|
+
|
|
164
|
+
def close(self) -> None:
|
|
165
|
+
if self.h:
|
|
166
|
+
self.lib.aios_close(self.h)
|
|
167
|
+
self.h = None
|
|
168
|
+
|
|
169
|
+
# --- agents ---
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def supports_client_tools(self) -> bool:
|
|
173
|
+
return self.abi_minor >= 3 and hasattr(self.lib, "aios_client_tool_complete")
|
|
174
|
+
|
|
175
|
+
def register(self, name: str) -> int:
|
|
176
|
+
"""Register this handle's agent. On ABI 4.3 kernels the ID is derived
|
|
177
|
+
from (UID, name), so it is the same after a restart."""
|
|
178
|
+
agent_id = ctypes.c_uint64(0)
|
|
179
|
+
if self.abi_minor >= 3 and hasattr(self.lib, "aios_register_agent_ex"):
|
|
180
|
+
if self.lib.aios_register_agent_ex(self.h, name.encode(), AGENT_REGISTER_STABLE,
|
|
181
|
+
ctypes.byref(agent_id)) < 0:
|
|
182
|
+
if ctypes.get_errno() == errno.EEXIST:
|
|
183
|
+
raise VectrosError(f"agent {name!r} is already running for this user; "
|
|
184
|
+
"stop it or use another name")
|
|
185
|
+
raise _errno_error("register agent")
|
|
186
|
+
elif self.lib.aios_register_agent(self.h, name.encode(), ctypes.byref(agent_id)) < 0:
|
|
187
|
+
raise _errno_error("register agent")
|
|
188
|
+
return agent_id.value
|
|
189
|
+
|
|
190
|
+
def unregister(self, agent_id: int) -> None:
|
|
191
|
+
if self.lib.aios_unregister_agent(self.h, agent_id) < 0:
|
|
192
|
+
raise _errno_error("unregister agent")
|
|
193
|
+
|
|
194
|
+
def setup_core(self, agent_id: int, core: int, pstr: str = "") -> None:
|
|
195
|
+
if self.lib.aios_setup_core(self.h, agent_id, core, 0, 0, pstr.encode()) < 0:
|
|
196
|
+
raise _errno_error(f"setup core {core}")
|
|
197
|
+
|
|
198
|
+
def trace_enabled(self, agent_id: int) -> bool:
|
|
199
|
+
function = getattr(self.lib, "aios_trace_get_enabled", None)
|
|
200
|
+
enabled = ctypes.c_int(0)
|
|
201
|
+
if function is None or function(self.h, agent_id, ctypes.byref(enabled)) < 0:
|
|
202
|
+
return False
|
|
203
|
+
return bool(enabled.value)
|
|
204
|
+
|
|
205
|
+
# --- raw syscalls ---
|
|
206
|
+
|
|
207
|
+
def submit(self, agent_id: int, type_id: int, payload: bytes, timeout_ms: int,
|
|
208
|
+
span: TraceSpan | None = None, keepalive: object = None) -> int:
|
|
209
|
+
buf = ctypes.create_string_buffer(payload, len(payload))
|
|
210
|
+
syscall_id = ctypes.c_uint64(0)
|
|
211
|
+
if span is not None and hasattr(self.lib, "aios_submit_trace"):
|
|
212
|
+
context = span.context()
|
|
213
|
+
query_id = int.from_bytes(span.trace_id[:8], "big") or 1
|
|
214
|
+
ret = self.lib.aios_submit_trace(
|
|
215
|
+
self.h, agent_id, query_id, type_id, 0, 1, ctypes.cast(buf, ctypes.c_void_p),
|
|
216
|
+
len(payload), timeout_ms, ctypes.byref(context), ctypes.byref(syscall_id))
|
|
217
|
+
else:
|
|
218
|
+
ret = self.lib.aios_submit(
|
|
219
|
+
self.h, agent_id, 0, type_id, 0, 1, ctypes.cast(buf, ctypes.c_void_p),
|
|
220
|
+
len(payload), timeout_ms, ctypes.byref(syscall_id))
|
|
221
|
+
if ret < 0:
|
|
222
|
+
error = _errno_error("submit syscall")
|
|
223
|
+
if span is not None:
|
|
224
|
+
span.finish(error=str(error))
|
|
225
|
+
raise error
|
|
226
|
+
self._inflight[syscall_id.value] = ((buf, keepalive), span)
|
|
227
|
+
return syscall_id.value
|
|
228
|
+
|
|
229
|
+
def wait(self, syscall_id: int) -> bytes:
|
|
230
|
+
result = ctypes.create_string_buffer(RESULT_MAX)
|
|
231
|
+
length = ctypes.c_uint32(RESULT_MAX)
|
|
232
|
+
state, error = ctypes.c_int(0), ctypes.c_int(0)
|
|
233
|
+
ret = self.lib.aios_wait(self.h, syscall_id, result, ctypes.byref(length),
|
|
234
|
+
ctypes.byref(state), ctypes.byref(error))
|
|
235
|
+
_, span = self._inflight.pop(syscall_id, (None, None))
|
|
236
|
+
if ret < 0:
|
|
237
|
+
failure = _errno_error("wait syscall")
|
|
238
|
+
if span is not None:
|
|
239
|
+
span.finish(error=str(failure))
|
|
240
|
+
raise failure
|
|
241
|
+
data = result.raw[:length.value]
|
|
242
|
+
extra = {"aios.completion.state": state.value}
|
|
243
|
+
if error.value:
|
|
244
|
+
message = data.split(b"\0", 1)[0].decode("utf-8", "replace")
|
|
245
|
+
failure = KernelError(abs(error.value), message or os.strerror(abs(error.value)))
|
|
246
|
+
if span is not None:
|
|
247
|
+
span.finish(error=str(failure), extra={**extra,
|
|
248
|
+
"aios.completion.error_code": error.value})
|
|
249
|
+
raise failure
|
|
250
|
+
if span is not None:
|
|
251
|
+
span.finish(output=data.decode("utf-8", "replace"), extra=extra)
|
|
252
|
+
return data
|
|
253
|
+
|
|
254
|
+
def status(self, syscall_id: int) -> tuple[int, int]:
|
|
255
|
+
state, error = ctypes.c_int(0), ctypes.c_int(0)
|
|
256
|
+
if self.lib.aios_query_status(self.h, syscall_id, ctypes.byref(state),
|
|
257
|
+
ctypes.byref(error)) < 0:
|
|
258
|
+
raise _errno_error("query status")
|
|
259
|
+
return state.value, error.value
|
|
260
|
+
|
|
261
|
+
def read_stream(self, syscall_id: int, size: int = 4096) -> tuple[bytes, int]:
|
|
262
|
+
buf = ctypes.create_string_buffer(size)
|
|
263
|
+
length = ctypes.c_uint32(size)
|
|
264
|
+
state, error = ctypes.c_int(0), ctypes.c_int(0)
|
|
265
|
+
if self.lib.aios_read_stream(self.h, syscall_id, buf, ctypes.byref(length), 1,
|
|
266
|
+
ctypes.byref(state), ctypes.byref(error)) < 0:
|
|
267
|
+
raise _errno_error("read stream")
|
|
268
|
+
return buf.raw[:length.value], state.value
|
|
269
|
+
|
|
270
|
+
def cancel(self, syscall_id: int) -> None:
|
|
271
|
+
if self.lib.aios_cancel(self.h, syscall_id) < 0:
|
|
272
|
+
raise _errno_error("cancel syscall")
|
|
273
|
+
|
|
274
|
+
def owner_action(self, agent_id: int, action: int, syscall_id: int,
|
|
275
|
+
reason: str = "") -> None:
|
|
276
|
+
request = struct.pack("@HHIIQQ128s", ABI_VERSION, 160, action, 0, agent_id,
|
|
277
|
+
syscall_id, reason.encode()[:127])
|
|
278
|
+
fcntl.ioctl(self.fd, _IOCTL_OWNER_ACTION, request)
|
|
279
|
+
|
|
280
|
+
# --- services used by Agent ---
|
|
281
|
+
|
|
282
|
+
def llm_stream(self, agent_id: int, messages: list[dict], model: str | None = None,
|
|
283
|
+
json_mode: bool = False, timeout_ms: int = 120_000,
|
|
284
|
+
span: TraceSpan | None = None,
|
|
285
|
+
tools: list[dict] | None = None) -> Generator[str, None, str]:
|
|
286
|
+
"""LLM syscall. Yields text chunks as the worker streams them and
|
|
287
|
+
returns the complete result. With tools, the worker returns a JSON
|
|
288
|
+
envelope {"aios_llm_result": 1, "content", "tool_calls"}."""
|
|
289
|
+
request = {
|
|
290
|
+
"messages": messages, "tools": tools or None,
|
|
291
|
+
"format": "json" if json_mode else None, "model": model,
|
|
292
|
+
}
|
|
293
|
+
if span is not None:
|
|
294
|
+
span.set_input(request)
|
|
295
|
+
payload = json.dumps(request).encode()
|
|
296
|
+
syscall_id = self.submit(agent_id, SYSCALL_LLM, payload, timeout_ms, span)
|
|
297
|
+
try:
|
|
298
|
+
while True:
|
|
299
|
+
chunk, state = self.read_stream(syscall_id)
|
|
300
|
+
if chunk:
|
|
301
|
+
yield chunk.decode("utf-8", "replace")
|
|
302
|
+
if state in TERMINAL_STATES and not chunk:
|
|
303
|
+
break
|
|
304
|
+
except GeneratorExit: # Caller stopped reading: free the kernel slot.
|
|
305
|
+
try:
|
|
306
|
+
self.cancel(syscall_id)
|
|
307
|
+
self.wait(syscall_id) # Reap the record; it fails with ECANCELED.
|
|
308
|
+
except KernelError:
|
|
309
|
+
pass
|
|
310
|
+
raise
|
|
311
|
+
return self.wait(syscall_id).decode("utf-8", "replace").rstrip("\0")
|
|
312
|
+
|
|
313
|
+
def tool(self, agent_id: int, name: str, args: dict, timeout_ms: int = 60_000,
|
|
314
|
+
approve: Callable[[str, dict], bool] | None = None,
|
|
315
|
+
span: TraceSpan | None = None) -> str:
|
|
316
|
+
"""Run a kernel-registered tool. Held side-effect tools go to approve();
|
|
317
|
+
with no approver they wait for the owner to decide in AIOS Manager."""
|
|
318
|
+
if span is not None:
|
|
319
|
+
span.set_input({"tool.name": name, "tool.arguments": args})
|
|
320
|
+
syscall_id = self.submit(agent_id, SYSCALL_TOOL, _tool_payload(name, args, 0),
|
|
321
|
+
timeout_ms, span)
|
|
322
|
+
if approve is not None:
|
|
323
|
+
while True:
|
|
324
|
+
state, _ = self.status(syscall_id)
|
|
325
|
+
if state == STATE_HELD_APPROVAL:
|
|
326
|
+
allowed = approve(name, args)
|
|
327
|
+
action = OWNER_APPROVE_TOOL if allowed else OWNER_DENY_TOOL
|
|
328
|
+
self.owner_action(agent_id, action, syscall_id,
|
|
329
|
+
"approved by agent owner" if allowed else "denied by agent owner")
|
|
330
|
+
break
|
|
331
|
+
if state not in (STATE_CREATED, STATE_QUEUED):
|
|
332
|
+
break
|
|
333
|
+
time.sleep(0.05)
|
|
334
|
+
try:
|
|
335
|
+
return self.wait(syscall_id).decode("utf-8", "replace").rstrip("\0")
|
|
336
|
+
except KernelError as exc:
|
|
337
|
+
# EACCES: no permission; ECANCELED: the owner (or approve()) denied it.
|
|
338
|
+
if exc.errno == errno.EACCES:
|
|
339
|
+
raise ToolDenied(name, str(exc)) from exc
|
|
340
|
+
if exc.errno == errno.ECANCELED:
|
|
341
|
+
raise ToolDenied(name, "denied or cancelled by the owner") from exc
|
|
342
|
+
raise
|
|
343
|
+
|
|
344
|
+
def client_tool(self, agent_id: int, name: str, args: dict, run: Callable[[], str],
|
|
345
|
+
approval: bool = False, approve: Callable[[str, dict], bool] | None = None,
|
|
346
|
+
timeout_ms: int = 60_000, span: TraceSpan | None = None) -> str:
|
|
347
|
+
"""Run a tool in this process under kernel control (ABI 4.3).
|
|
348
|
+
|
|
349
|
+
The kernel checks TOOL core permission, applies the deadline, records
|
|
350
|
+
the call in telemetry and, with approval, holds it until the owner
|
|
351
|
+
decides (through approve(), or in AIOS Manager when approve is None).
|
|
352
|
+
run() executes only once the kernel hands the call over.
|
|
353
|
+
"""
|
|
354
|
+
if span is not None:
|
|
355
|
+
span.set_input({"tool.name": name, "tool.arguments": args})
|
|
356
|
+
flags = TOOL_CALL_CLIENT | (TOOL_CALL_APPROVAL if approval else 0)
|
|
357
|
+
syscall_id = self.submit(agent_id, SYSCALL_TOOL, _tool_payload(name, args, flags),
|
|
358
|
+
timeout_ms, span)
|
|
359
|
+
asked = False
|
|
360
|
+
while True:
|
|
361
|
+
state, _ = self.status(syscall_id)
|
|
362
|
+
if state == STATE_HELD_APPROVAL and approve is not None and not asked:
|
|
363
|
+
asked = True
|
|
364
|
+
allowed = approve(name, args)
|
|
365
|
+
self.owner_action(agent_id, OWNER_APPROVE_TOOL if allowed else OWNER_DENY_TOOL,
|
|
366
|
+
syscall_id, "approved by agent owner" if allowed
|
|
367
|
+
else "denied by agent owner")
|
|
368
|
+
continue
|
|
369
|
+
if state == STATE_PROCESSING or state in TERMINAL_STATES:
|
|
370
|
+
break
|
|
371
|
+
time.sleep(0.05)
|
|
372
|
+
if state == STATE_PROCESSING:
|
|
373
|
+
try:
|
|
374
|
+
result = run()
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
self._client_complete(syscall_id, f"{type(exc).__name__}: {exc}".encode()[:1024],
|
|
377
|
+
-errno.EIO)
|
|
378
|
+
try:
|
|
379
|
+
self.wait(syscall_id)
|
|
380
|
+
except KernelError:
|
|
381
|
+
pass
|
|
382
|
+
raise
|
|
383
|
+
self._client_complete(syscall_id, result.encode()[:CLIENT_RESULT_MAX], 0)
|
|
384
|
+
try:
|
|
385
|
+
return self.wait(syscall_id).decode("utf-8", "replace")
|
|
386
|
+
except KernelError as exc:
|
|
387
|
+
if exc.errno in (errno.ECANCELED, errno.EACCES):
|
|
388
|
+
raise ToolDenied(name, "denied or cancelled by the owner") from exc
|
|
389
|
+
raise
|
|
390
|
+
|
|
391
|
+
def _client_complete(self, syscall_id: int, data: bytes, error: int) -> None:
|
|
392
|
+
buf = ctypes.create_string_buffer(data, max(1, len(data)))
|
|
393
|
+
if self.lib.aios_client_tool_complete(self.h, syscall_id, buf, len(data), error) < 0:
|
|
394
|
+
# ECANCELED: the deadline passed or the owner cancelled meanwhile;
|
|
395
|
+
# wait() reports which.
|
|
396
|
+
if ctypes.get_errno() != errno.ECANCELED:
|
|
397
|
+
raise _errno_error("complete client tool")
|
|
398
|
+
|
|
399
|
+
def list_tools(self) -> list[dict]:
|
|
400
|
+
"""Kernel tool registry, all pages."""
|
|
401
|
+
size = _TOOL_PAGE_HEADER.size + 8 * _TOOL_ENTRY.size
|
|
402
|
+
request = 0xC0000000 | (size << 16) | (ord("A") << 8) | _IOCTL_LIST_TOOLS_NR
|
|
403
|
+
tools, offset = [], 0
|
|
404
|
+
while True:
|
|
405
|
+
buf = bytearray(_TOOL_PAGE_HEADER.pack(ABI_VERSION, size, 0, offset, 0, 0, 0)
|
|
406
|
+
+ bytes(8 * _TOOL_ENTRY.size))
|
|
407
|
+
fcntl.ioctl(self.fd, request, buf, True)
|
|
408
|
+
_, _, _, _, total, count, _ = _TOOL_PAGE_HEADER.unpack_from(buf)
|
|
409
|
+
for index in range(count):
|
|
410
|
+
fields = _TOOL_ENTRY.unpack_from(buf, _TOOL_PAGE_HEADER.size + index * _TOOL_ENTRY.size)
|
|
411
|
+
schema_text = fields[5].split(b"\0", 1)[0].decode("utf-8", "replace")
|
|
412
|
+
try:
|
|
413
|
+
schema = json.loads(schema_text) if schema_text else {}
|
|
414
|
+
except json.JSONDecodeError:
|
|
415
|
+
schema = {}
|
|
416
|
+
tools.append({
|
|
417
|
+
"name": fields[3].split(b"\0", 1)[0].decode(),
|
|
418
|
+
"schema": schema if isinstance(schema, dict) else {},
|
|
419
|
+
"approval_required": bool(fields[2] & 1),
|
|
420
|
+
})
|
|
421
|
+
offset += count
|
|
422
|
+
if not count or offset >= total:
|
|
423
|
+
return tools
|
|
424
|
+
|
|
425
|
+
def storage_write(self, agent_id: int, path: str, data: bytes,
|
|
426
|
+
timeout_ms: int = 10_000) -> None:
|
|
427
|
+
self._storage(agent_id, STO_WRITE, path, data, timeout_ms)
|
|
428
|
+
|
|
429
|
+
def storage_read(self, agent_id: int, path: str, timeout_ms: int = 10_000) -> bytes:
|
|
430
|
+
return self._storage(agent_id, STO_RETRIEVE, path, b"", timeout_ms)
|
|
431
|
+
|
|
432
|
+
def _storage(self, agent_id: int, op: int, path: str, data: bytes,
|
|
433
|
+
timeout_ms: int) -> bytes:
|
|
434
|
+
raw_path = path.encode()
|
|
435
|
+
if not raw_path or len(raw_path) >= 256 or b"\0" in raw_path:
|
|
436
|
+
raise ValueError("storage path must be 1-255 UTF-8 bytes without NUL")
|
|
437
|
+
source = ctypes.create_string_buffer(data, len(data)) if data else None
|
|
438
|
+
pointer = ctypes.addressof(source) if source is not None else 0
|
|
439
|
+
payload = _STORAGE_HEADER.pack(op, raw_path, pointer, len(data), 0, 0)
|
|
440
|
+
return self.wait(self.submit(agent_id, SYSCALL_STORAGE, payload, timeout_ms,
|
|
441
|
+
keepalive=source))
|