ai-security-school-sdk 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.
- ai_security_school_sdk-0.1.0/PKG-INFO +206 -0
- ai_security_school_sdk-0.1.0/README.md +190 -0
- ai_security_school_sdk-0.1.0/pyproject.toml +54 -0
- ai_security_school_sdk-0.1.0/pyproject.toml.orig +42 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/__init__.py +67 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/_handles.py +113 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/_http.py +141 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/_schema.py +56 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/async_client.py +384 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/client.py +381 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/errors.py +121 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/models.py +123 -0
- ai_security_school_sdk-0.1.0/src/ai_security_school_sdk/py.typed +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ai-security-school-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for AI Security School learner actions and isolated agent experiments
|
|
5
|
+
Author: germankochnev
|
|
6
|
+
Author-email: germankochnev <kochgerm@gmail.com>
|
|
7
|
+
Requires-Dist: httpx>=0.28,<1
|
|
8
|
+
Requires-Dist: pydantic>=2.10,<3
|
|
9
|
+
Requires-Dist: jsonschema>=4.23,<5
|
|
10
|
+
Requires-Dist: referencing>=0.35,<1
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Project-URL: Homepage, https://github.com/ai-security-lab-itmo/ai-security-school-sdk
|
|
13
|
+
Project-URL: Documentation, https://github.com/ai-security-lab-itmo/ai-security-school-sdk#readme
|
|
14
|
+
Project-URL: Issues, https://github.com/ai-security-lab-itmo/ai-security-school-sdk/issues
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# AI Security School SDK
|
|
18
|
+
|
|
19
|
+
Python-клиент для учебных агентских лабораторий. Скрипт атаки работает на вашей
|
|
20
|
+
машине, а SDK вызывает явно доступные **действия студента** в персональном прогоне
|
|
21
|
+
на платформе. Внутренние инструменты атакуемого агента через SDK не публикуются.
|
|
22
|
+
|
|
23
|
+
Требуется Python 3.12+. Исходники и релизы доступны в
|
|
24
|
+
[публичном репозитории](https://github.com/ai-security-lab-itmo/ai-security-school-sdk).
|
|
25
|
+
|
|
26
|
+
## Установка и токен
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install "git+https://github.com/ai-security-lab-itmo/ai-security-school-sdk.git@v0.1.0"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Для установки из Git нужен установленный Git. Альтернатива — готовый wheel
|
|
33
|
+
из релиза, который можно установить без Git:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python -m pip install "https://github.com/ai-security-lab-itmo/ai-security-school-sdk/releases/download/v0.1.0/ai_security_school_sdk-0.1.0-py3-none-any.whl"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Обе команды устанавливают зафиксированную версию `0.1.0`. Установка по короткому
|
|
40
|
+
имени `pip install ai-security-school-sdk` станет доступна после публикации в
|
|
41
|
+
PyPI; сейчас используйте одну из команд выше. Настройка публикации описана в
|
|
42
|
+
[PUBLISHING.md](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/main/PUBLISHING.md).
|
|
43
|
+
|
|
44
|
+
На странице операции выберите «Подключить Python SDK» и получите токен. Он
|
|
45
|
+
ограничен одной операцией и имеет срок действия. Передайте его через переменную
|
|
46
|
+
окружения `AI_SECURITY_SCHOOL_TOKEN`; не сохраняйте токен в коде или репозитории.
|
|
47
|
+
Необязательная `AI_SECURITY_SCHOOL_BASE_URL` по умолчанию равна
|
|
48
|
+
`https://plgn.aisecschool.ru`. Для удалённых серверов необходим HTTPS.
|
|
49
|
+
|
|
50
|
+
## Первый вызов
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ai_security_school_sdk import Client
|
|
54
|
+
|
|
55
|
+
with Client.from_env() as client:
|
|
56
|
+
for available in client.labs.list():
|
|
57
|
+
print(available.lab_id, available.title)
|
|
58
|
+
|
|
59
|
+
lab = client.labs.get("YOUR_LAB_ID")
|
|
60
|
+
run = lab.runs.create()
|
|
61
|
+
print("Сохраните run_id для продолжения:", run.run_id)
|
|
62
|
+
|
|
63
|
+
for action in run.actions.list():
|
|
64
|
+
print(action.name, action.description)
|
|
65
|
+
print(action.input_schema)
|
|
66
|
+
print(action.examples)
|
|
67
|
+
|
|
68
|
+
# Имя и аргументы выбираются из manifest текущей CTF.
|
|
69
|
+
result = run.actions.call("send_message", {"message": "Проверь новый документ"})
|
|
70
|
+
print(result.data)
|
|
71
|
+
print(run.observation().state)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`send_message` здесь — пример имени, а не встроенный метод SDK. Конкретные CTF
|
|
75
|
+
могут предоставлять разные действия: добавление документа, сообщение агенту,
|
|
76
|
+
загрузку вложения и другие операции. SDK получает их имена и JSON Schema от
|
|
77
|
+
сервера. Новый набор действий не требует новой версии Python-пакета.
|
|
78
|
+
|
|
79
|
+
Вызов проверяет аргументы локально и передаёт `expected_task_id`. Сервер повторно
|
|
80
|
+
проверяет действие, права и аргументы. SDK не загружает внешние ссылки JSON Schema
|
|
81
|
+
и не исполняет код из manifest. Если этап изменился через другой клиент, вызов
|
|
82
|
+
возвращает `ConflictError`; явно выполните `run.refresh()` и изучите новый набор.
|
|
83
|
+
|
|
84
|
+
Внешние поверхности атаки, например реестр пакетов или MCP-сервис, не перечисляются
|
|
85
|
+
автоматически. Если они входят в сценарий, взаимодействуйте с ними через их
|
|
86
|
+
собственные интерфейсы; SDK управляет только действиями, опубликованными CTF.
|
|
87
|
+
|
|
88
|
+
## Прогоны, этапы и ветвление
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with Client.from_env() as client:
|
|
92
|
+
run = client.runs.get("SAVED_RUN_ID")
|
|
93
|
+
checkpoint = run.checkpoint()
|
|
94
|
+
branch = checkpoint.fork()
|
|
95
|
+
print(branch.run_id, branch.task_id)
|
|
96
|
+
|
|
97
|
+
# Здесь выполняются доступные действия атаки.
|
|
98
|
+
verdict = branch.submit()
|
|
99
|
+
print(verdict.passed, verdict.success_rate)
|
|
100
|
+
if verdict.passed:
|
|
101
|
+
branch.advance() # Явный переход, если есть следующий этап.
|
|
102
|
+
print(branch.actions.list())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Этапы одной ветки разделяют состояние. Разные прогоны и forks независимы. Внутри
|
|
106
|
+
одного прогона одновременно исполняется одно задание; для параллельного поиска
|
|
107
|
+
создавайте отдельные прогоны. Новый прогон не обновляет общий бюджет пользователя.
|
|
108
|
+
Checkpoint доступен для свободного прогона; fork сохраняет его состояние и этап.
|
|
109
|
+
|
|
110
|
+
`submit()` сдаёт записанный сервером результат вашей атаки. Проверка может
|
|
111
|
+
повторять действия на скрытых сценариях. Загружать Python-программу для исполнения
|
|
112
|
+
на сервере не требуется. Успешный обычный вызов действия сам по себе не даёт зачёт.
|
|
113
|
+
|
|
114
|
+
`run.close()` закрывает серверный прогон явно. Выход из `with Client(...)` закрывает
|
|
115
|
+
только HTTP-соединения и сохраняет прогон для продолжения.
|
|
116
|
+
|
|
117
|
+
## Долгие задания, повторы и ошибки
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from ai_security_school_sdk import Client, JobTimeoutError
|
|
121
|
+
|
|
122
|
+
with Client.from_env() as client:
|
|
123
|
+
run = client.runs.get("SAVED_RUN_ID")
|
|
124
|
+
job = run.actions.start_call("send_message", {"message": "Обработай заявку"})
|
|
125
|
+
print("Сохраните job_id:", job.job_id)
|
|
126
|
+
try:
|
|
127
|
+
result = job.wait(timeout=120, poll_interval=0.5)
|
|
128
|
+
except JobTimeoutError as error:
|
|
129
|
+
# Истечение времени ожидания не отменяет серверное задание.
|
|
130
|
+
resumed = client.jobs.get(error.job_id)
|
|
131
|
+
result = resumed.wait(timeout=120)
|
|
132
|
+
print(result)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `run.actions.call()` и `run.submit()` запускают задание и ждут результат.
|
|
136
|
+
`start_call()` и `start_submission()` сразу возвращают handle задания.
|
|
137
|
+
- `client.jobs.get(id)`, `job.refresh()`, `job.cancel()` и `job.result()` позволяют
|
|
138
|
+
управлять уже созданным заданием. Отмена не возвращает стоимость LLM-запросов,
|
|
139
|
+
которые уже отправлены.
|
|
140
|
+
- Все изменения имеют `Idempotency-Key`. Сетевые повторы используют тот же ключ
|
|
141
|
+
и тело; SDK никогда не создаёт новый ключ внутри повторного запроса.
|
|
142
|
+
- Для восстановления после завершения процесса передайте сохранённый
|
|
143
|
+
`idempotency_key=`. `TransportError.idempotency_key` содержит ключ запроса с
|
|
144
|
+
неопределённым результатом. С тем же ключом повторяйте только то же действие,
|
|
145
|
+
аргументы и текущую CTF; не создавайте новую попытку вслепую.
|
|
146
|
+
- По умолчанию доступны два повтора при сетевой ошибке и HTTP 429/502/503/504.
|
|
147
|
+
Параметры клиента: `timeout=30`, `max_retries=2`, `retry_backoff=0.25`.
|
|
148
|
+
Серверные HTTP-ошибки и ошибки самого задания после polling не запускают новую
|
|
149
|
+
задачу.
|
|
150
|
+
- `JobTimeoutError` содержит `job_id`. `JobInterruptedError` означает, что
|
|
151
|
+
безопасное автоматическое продолжение исполнения невозможно; изучите историю.
|
|
152
|
+
- `AuthenticationError`, `PermissionDeniedError`, `ConflictError`,
|
|
153
|
+
`StageLockedError`, `LimitExceededError` наследуют `APIError` с полями `code`,
|
|
154
|
+
`message`, `details`, `status_code`, `job_id`.
|
|
155
|
+
- `ActionValidationError` описывает локальное несоответствие схеме, а
|
|
156
|
+
`ProtocolError` — некорректный ответ или неподдерживаемую ссылку схемы.
|
|
157
|
+
|
|
158
|
+
## Async и наблюдения
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import asyncio
|
|
162
|
+
from ai_security_school_sdk import AsyncClient
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def main():
|
|
166
|
+
async with AsyncClient.from_env() as client:
|
|
167
|
+
lab = await client.labs.get("YOUR_LAB_ID")
|
|
168
|
+
run = await lab.runs.create()
|
|
169
|
+
actions = await run.actions.list()
|
|
170
|
+
print(actions)
|
|
171
|
+
observation = await run.observation()
|
|
172
|
+
page = await run.events(after=0)
|
|
173
|
+
print(observation.state, observation.usage)
|
|
174
|
+
for event in page.events:
|
|
175
|
+
print(event.sequence, event.kind, event.data)
|
|
176
|
+
# Следующая порция: await run.events(after=page.next_cursor)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
asyncio.run(main())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Все методы с сетевым вводом-выводом у `AsyncClient` вызываются через `await`.
|
|
183
|
+
Конструкторы, `from_env()`, поля объектов и `job.result()` синхронные. Asyncio
|
|
184
|
+
отмена локальной coroutine не отменяет серверное задание; сохраняйте `job_id`.
|
|
185
|
+
Примеры: [первый эксперимент](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/first_experiment.py),
|
|
186
|
+
[параллельный поиск](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/async_search.py),
|
|
187
|
+
[этапы и fork](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/multistage.py).
|
|
188
|
+
|
|
189
|
+
Объекты содержат типизированный снимок в `.info`. Методы `refresh()` обновляют его;
|
|
190
|
+
для актуального состояния сервера не полагайтесь на старый снимок. Аргументы и
|
|
191
|
+
результаты конкретных действий — обычные JSON-объекты. Новые дополнительные поля
|
|
192
|
+
общих серверных моделей допускаются для совместимости.
|
|
193
|
+
|
|
194
|
+
## Разработка
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
uv sync --python 3.12
|
|
198
|
+
uv run pytest
|
|
199
|
+
uv run ruff check .
|
|
200
|
+
uv run mypy src
|
|
201
|
+
uv build
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Пакет не импортирует backend платформы. Тесты используют HTTPX MockTransport и
|
|
205
|
+
проверяют общий HTTP-контракт sync/async клиентов без LLM-вызовов. API имеет базу
|
|
206
|
+
`/api/learner/v1`; его версия не зависит от номера выпуска SDK.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# AI Security School SDK
|
|
2
|
+
|
|
3
|
+
Python-клиент для учебных агентских лабораторий. Скрипт атаки работает на вашей
|
|
4
|
+
машине, а SDK вызывает явно доступные **действия студента** в персональном прогоне
|
|
5
|
+
на платформе. Внутренние инструменты атакуемого агента через SDK не публикуются.
|
|
6
|
+
|
|
7
|
+
Требуется Python 3.12+. Исходники и релизы доступны в
|
|
8
|
+
[публичном репозитории](https://github.com/ai-security-lab-itmo/ai-security-school-sdk).
|
|
9
|
+
|
|
10
|
+
## Установка и токен
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
python -m pip install "git+https://github.com/ai-security-lab-itmo/ai-security-school-sdk.git@v0.1.0"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Для установки из Git нужен установленный Git. Альтернатива — готовый wheel
|
|
17
|
+
из релиза, который можно установить без Git:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python -m pip install "https://github.com/ai-security-lab-itmo/ai-security-school-sdk/releases/download/v0.1.0/ai_security_school_sdk-0.1.0-py3-none-any.whl"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Обе команды устанавливают зафиксированную версию `0.1.0`. Установка по короткому
|
|
24
|
+
имени `pip install ai-security-school-sdk` станет доступна после публикации в
|
|
25
|
+
PyPI; сейчас используйте одну из команд выше. Настройка публикации описана в
|
|
26
|
+
[PUBLISHING.md](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/main/PUBLISHING.md).
|
|
27
|
+
|
|
28
|
+
На странице операции выберите «Подключить Python SDK» и получите токен. Он
|
|
29
|
+
ограничен одной операцией и имеет срок действия. Передайте его через переменную
|
|
30
|
+
окружения `AI_SECURITY_SCHOOL_TOKEN`; не сохраняйте токен в коде или репозитории.
|
|
31
|
+
Необязательная `AI_SECURITY_SCHOOL_BASE_URL` по умолчанию равна
|
|
32
|
+
`https://plgn.aisecschool.ru`. Для удалённых серверов необходим HTTPS.
|
|
33
|
+
|
|
34
|
+
## Первый вызов
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from ai_security_school_sdk import Client
|
|
38
|
+
|
|
39
|
+
with Client.from_env() as client:
|
|
40
|
+
for available in client.labs.list():
|
|
41
|
+
print(available.lab_id, available.title)
|
|
42
|
+
|
|
43
|
+
lab = client.labs.get("YOUR_LAB_ID")
|
|
44
|
+
run = lab.runs.create()
|
|
45
|
+
print("Сохраните run_id для продолжения:", run.run_id)
|
|
46
|
+
|
|
47
|
+
for action in run.actions.list():
|
|
48
|
+
print(action.name, action.description)
|
|
49
|
+
print(action.input_schema)
|
|
50
|
+
print(action.examples)
|
|
51
|
+
|
|
52
|
+
# Имя и аргументы выбираются из manifest текущей CTF.
|
|
53
|
+
result = run.actions.call("send_message", {"message": "Проверь новый документ"})
|
|
54
|
+
print(result.data)
|
|
55
|
+
print(run.observation().state)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`send_message` здесь — пример имени, а не встроенный метод SDK. Конкретные CTF
|
|
59
|
+
могут предоставлять разные действия: добавление документа, сообщение агенту,
|
|
60
|
+
загрузку вложения и другие операции. SDK получает их имена и JSON Schema от
|
|
61
|
+
сервера. Новый набор действий не требует новой версии Python-пакета.
|
|
62
|
+
|
|
63
|
+
Вызов проверяет аргументы локально и передаёт `expected_task_id`. Сервер повторно
|
|
64
|
+
проверяет действие, права и аргументы. SDK не загружает внешние ссылки JSON Schema
|
|
65
|
+
и не исполняет код из manifest. Если этап изменился через другой клиент, вызов
|
|
66
|
+
возвращает `ConflictError`; явно выполните `run.refresh()` и изучите новый набор.
|
|
67
|
+
|
|
68
|
+
Внешние поверхности атаки, например реестр пакетов или MCP-сервис, не перечисляются
|
|
69
|
+
автоматически. Если они входят в сценарий, взаимодействуйте с ними через их
|
|
70
|
+
собственные интерфейсы; SDK управляет только действиями, опубликованными CTF.
|
|
71
|
+
|
|
72
|
+
## Прогоны, этапы и ветвление
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
with Client.from_env() as client:
|
|
76
|
+
run = client.runs.get("SAVED_RUN_ID")
|
|
77
|
+
checkpoint = run.checkpoint()
|
|
78
|
+
branch = checkpoint.fork()
|
|
79
|
+
print(branch.run_id, branch.task_id)
|
|
80
|
+
|
|
81
|
+
# Здесь выполняются доступные действия атаки.
|
|
82
|
+
verdict = branch.submit()
|
|
83
|
+
print(verdict.passed, verdict.success_rate)
|
|
84
|
+
if verdict.passed:
|
|
85
|
+
branch.advance() # Явный переход, если есть следующий этап.
|
|
86
|
+
print(branch.actions.list())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Этапы одной ветки разделяют состояние. Разные прогоны и forks независимы. Внутри
|
|
90
|
+
одного прогона одновременно исполняется одно задание; для параллельного поиска
|
|
91
|
+
создавайте отдельные прогоны. Новый прогон не обновляет общий бюджет пользователя.
|
|
92
|
+
Checkpoint доступен для свободного прогона; fork сохраняет его состояние и этап.
|
|
93
|
+
|
|
94
|
+
`submit()` сдаёт записанный сервером результат вашей атаки. Проверка может
|
|
95
|
+
повторять действия на скрытых сценариях. Загружать Python-программу для исполнения
|
|
96
|
+
на сервере не требуется. Успешный обычный вызов действия сам по себе не даёт зачёт.
|
|
97
|
+
|
|
98
|
+
`run.close()` закрывает серверный прогон явно. Выход из `with Client(...)` закрывает
|
|
99
|
+
только HTTP-соединения и сохраняет прогон для продолжения.
|
|
100
|
+
|
|
101
|
+
## Долгие задания, повторы и ошибки
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from ai_security_school_sdk import Client, JobTimeoutError
|
|
105
|
+
|
|
106
|
+
with Client.from_env() as client:
|
|
107
|
+
run = client.runs.get("SAVED_RUN_ID")
|
|
108
|
+
job = run.actions.start_call("send_message", {"message": "Обработай заявку"})
|
|
109
|
+
print("Сохраните job_id:", job.job_id)
|
|
110
|
+
try:
|
|
111
|
+
result = job.wait(timeout=120, poll_interval=0.5)
|
|
112
|
+
except JobTimeoutError as error:
|
|
113
|
+
# Истечение времени ожидания не отменяет серверное задание.
|
|
114
|
+
resumed = client.jobs.get(error.job_id)
|
|
115
|
+
result = resumed.wait(timeout=120)
|
|
116
|
+
print(result)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `run.actions.call()` и `run.submit()` запускают задание и ждут результат.
|
|
120
|
+
`start_call()` и `start_submission()` сразу возвращают handle задания.
|
|
121
|
+
- `client.jobs.get(id)`, `job.refresh()`, `job.cancel()` и `job.result()` позволяют
|
|
122
|
+
управлять уже созданным заданием. Отмена не возвращает стоимость LLM-запросов,
|
|
123
|
+
которые уже отправлены.
|
|
124
|
+
- Все изменения имеют `Idempotency-Key`. Сетевые повторы используют тот же ключ
|
|
125
|
+
и тело; SDK никогда не создаёт новый ключ внутри повторного запроса.
|
|
126
|
+
- Для восстановления после завершения процесса передайте сохранённый
|
|
127
|
+
`idempotency_key=`. `TransportError.idempotency_key` содержит ключ запроса с
|
|
128
|
+
неопределённым результатом. С тем же ключом повторяйте только то же действие,
|
|
129
|
+
аргументы и текущую CTF; не создавайте новую попытку вслепую.
|
|
130
|
+
- По умолчанию доступны два повтора при сетевой ошибке и HTTP 429/502/503/504.
|
|
131
|
+
Параметры клиента: `timeout=30`, `max_retries=2`, `retry_backoff=0.25`.
|
|
132
|
+
Серверные HTTP-ошибки и ошибки самого задания после polling не запускают новую
|
|
133
|
+
задачу.
|
|
134
|
+
- `JobTimeoutError` содержит `job_id`. `JobInterruptedError` означает, что
|
|
135
|
+
безопасное автоматическое продолжение исполнения невозможно; изучите историю.
|
|
136
|
+
- `AuthenticationError`, `PermissionDeniedError`, `ConflictError`,
|
|
137
|
+
`StageLockedError`, `LimitExceededError` наследуют `APIError` с полями `code`,
|
|
138
|
+
`message`, `details`, `status_code`, `job_id`.
|
|
139
|
+
- `ActionValidationError` описывает локальное несоответствие схеме, а
|
|
140
|
+
`ProtocolError` — некорректный ответ или неподдерживаемую ссылку схемы.
|
|
141
|
+
|
|
142
|
+
## Async и наблюдения
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import asyncio
|
|
146
|
+
from ai_security_school_sdk import AsyncClient
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def main():
|
|
150
|
+
async with AsyncClient.from_env() as client:
|
|
151
|
+
lab = await client.labs.get("YOUR_LAB_ID")
|
|
152
|
+
run = await lab.runs.create()
|
|
153
|
+
actions = await run.actions.list()
|
|
154
|
+
print(actions)
|
|
155
|
+
observation = await run.observation()
|
|
156
|
+
page = await run.events(after=0)
|
|
157
|
+
print(observation.state, observation.usage)
|
|
158
|
+
for event in page.events:
|
|
159
|
+
print(event.sequence, event.kind, event.data)
|
|
160
|
+
# Следующая порция: await run.events(after=page.next_cursor)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
asyncio.run(main())
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Все методы с сетевым вводом-выводом у `AsyncClient` вызываются через `await`.
|
|
167
|
+
Конструкторы, `from_env()`, поля объектов и `job.result()` синхронные. Asyncio
|
|
168
|
+
отмена локальной coroutine не отменяет серверное задание; сохраняйте `job_id`.
|
|
169
|
+
Примеры: [первый эксперимент](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/first_experiment.py),
|
|
170
|
+
[параллельный поиск](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/async_search.py),
|
|
171
|
+
[этапы и fork](https://github.com/ai-security-lab-itmo/ai-security-school-sdk/blob/v0.1.0/examples/multistage.py).
|
|
172
|
+
|
|
173
|
+
Объекты содержат типизированный снимок в `.info`. Методы `refresh()` обновляют его;
|
|
174
|
+
для актуального состояния сервера не полагайтесь на старый снимок. Аргументы и
|
|
175
|
+
результаты конкретных действий — обычные JSON-объекты. Новые дополнительные поля
|
|
176
|
+
общих серверных моделей допускаются для совместимости.
|
|
177
|
+
|
|
178
|
+
## Разработка
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
uv sync --python 3.12
|
|
182
|
+
uv run pytest
|
|
183
|
+
uv run ruff check .
|
|
184
|
+
uv run mypy src
|
|
185
|
+
uv build
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Пакет не импортирует backend платформы. Тесты используют HTTPX MockTransport и
|
|
189
|
+
проверяют общий HTTP-контракт sync/async клиентов без LLM-вызовов. API имеет базу
|
|
190
|
+
`/api/learner/v1`; его версия не зависит от номера выпуска SDK.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ai-security-school-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for AI Security School learner actions and isolated agent experiments"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28,<1",
|
|
9
|
+
"pydantic>=2.10,<3",
|
|
10
|
+
"jsonschema>=4.23,<5",
|
|
11
|
+
"referencing>=0.35,<1",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "germankochnev"
|
|
16
|
+
email = "kochgerm@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk"
|
|
20
|
+
Documentation = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk#readme"
|
|
21
|
+
Issues = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk/issues"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.3,<9",
|
|
26
|
+
"pytest-asyncio>=0.25,<2",
|
|
27
|
+
"ruff>=0.9,<1",
|
|
28
|
+
"mypy>=1.15,<2",
|
|
29
|
+
"types-jsonschema>=4.23",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py312"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = [
|
|
42
|
+
"E",
|
|
43
|
+
"F",
|
|
44
|
+
"I",
|
|
45
|
+
"UP",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
python_version = "3.12"
|
|
50
|
+
strict = true
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
54
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ai-security-school-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for AI Security School learner actions and isolated agent experiments"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "germankochnev", email = "kochgerm@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.28,<1",
|
|
12
|
+
"pydantic>=2.10,<3",
|
|
13
|
+
"jsonschema>=4.23,<5",
|
|
14
|
+
"referencing>=0.35,<1",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk"
|
|
19
|
+
Documentation = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk#readme"
|
|
20
|
+
Issues = "https://github.com/ai-security-lab-itmo/ai-security-school-sdk/issues"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest>=8.3,<9", "pytest-asyncio>=0.25,<2", "ruff>=0.9,<1", "mypy>=1.15,<2", "types-jsonschema>=4.23"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
asyncio_mode = "auto"
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 100
|
|
31
|
+
target-version = "py312"
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "I", "UP"]
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
python_version = "3.12"
|
|
38
|
+
strict = true
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
42
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""AI Security School learner SDK: explicit student actions, isolated experiments."""
|
|
2
|
+
|
|
3
|
+
from .async_client import AsyncClient
|
|
4
|
+
from .client import Client
|
|
5
|
+
from .errors import (
|
|
6
|
+
ActionValidationError,
|
|
7
|
+
APIError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ConfigurationError,
|
|
10
|
+
ConflictError,
|
|
11
|
+
JobCancelledError,
|
|
12
|
+
JobInterruptedError,
|
|
13
|
+
JobTimeoutError,
|
|
14
|
+
LimitExceededError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
PermissionDeniedError,
|
|
17
|
+
ProtocolError,
|
|
18
|
+
SDKError,
|
|
19
|
+
StageLockedError,
|
|
20
|
+
TransportError,
|
|
21
|
+
)
|
|
22
|
+
from .models import (
|
|
23
|
+
ActionDescriptor,
|
|
24
|
+
CallResult,
|
|
25
|
+
Checkpoint,
|
|
26
|
+
Event,
|
|
27
|
+
EventPage,
|
|
28
|
+
Job,
|
|
29
|
+
Lab,
|
|
30
|
+
Observation,
|
|
31
|
+
Run,
|
|
32
|
+
SubmissionResult,
|
|
33
|
+
Task,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"APIError",
|
|
40
|
+
"ActionDescriptor",
|
|
41
|
+
"ActionValidationError",
|
|
42
|
+
"AsyncClient",
|
|
43
|
+
"AuthenticationError",
|
|
44
|
+
"CallResult",
|
|
45
|
+
"Checkpoint",
|
|
46
|
+
"Client",
|
|
47
|
+
"ConfigurationError",
|
|
48
|
+
"ConflictError",
|
|
49
|
+
"Event",
|
|
50
|
+
"EventPage",
|
|
51
|
+
"Job",
|
|
52
|
+
"JobCancelledError",
|
|
53
|
+
"JobInterruptedError",
|
|
54
|
+
"JobTimeoutError",
|
|
55
|
+
"Lab",
|
|
56
|
+
"LimitExceededError",
|
|
57
|
+
"NotFoundError",
|
|
58
|
+
"Observation",
|
|
59
|
+
"PermissionDeniedError",
|
|
60
|
+
"ProtocolError",
|
|
61
|
+
"Run",
|
|
62
|
+
"SDKError",
|
|
63
|
+
"StageLockedError",
|
|
64
|
+
"SubmissionResult",
|
|
65
|
+
"Task",
|
|
66
|
+
"TransportError",
|
|
67
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Pure shared behavior for resource handles."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from ._http import parse_model
|
|
7
|
+
from .errors import ProtocolError, api_error
|
|
8
|
+
from .models import CallResult, Checkpoint, Job, Lab, Run, SubmissionResult, Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LabHandle:
|
|
12
|
+
def __init__(self, info: Lab) -> None:
|
|
13
|
+
self.info = info
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def lab_id(self) -> str:
|
|
17
|
+
return self.info.lab_id
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def title(self) -> str:
|
|
21
|
+
return self.info.title
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def description(self) -> str:
|
|
25
|
+
return self.info.description
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def stages(self) -> list[Task]:
|
|
29
|
+
return self.info.stages
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RunHandle:
|
|
33
|
+
def __init__(self, info: Run) -> None:
|
|
34
|
+
self.info = info
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def run_id(self) -> str:
|
|
38
|
+
return self.info.run_id
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def lab_id(self) -> str:
|
|
42
|
+
return self.info.lab_id
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def task_id(self) -> str:
|
|
46
|
+
return self.info.task_id
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def stage_index(self) -> int:
|
|
50
|
+
return self.info.stage_index
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def status(self) -> Literal["idle", "busy", "closed"]:
|
|
54
|
+
return self.info.status
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def revision(self) -> int:
|
|
58
|
+
return self.info.revision
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def stage_passed(self) -> bool:
|
|
62
|
+
return self.info.stage_passed
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def created_at(self) -> datetime:
|
|
66
|
+
return self.info.created_at
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CheckpointHandle:
|
|
70
|
+
def __init__(self, info: Checkpoint) -> None:
|
|
71
|
+
self.info = info
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def checkpoint_id(self) -> str:
|
|
75
|
+
return self.info.checkpoint_id
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class JobHandle:
|
|
79
|
+
def __init__(self, info: Job) -> None:
|
|
80
|
+
self.info = info
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def job_id(self) -> str:
|
|
84
|
+
return self.info.job_id
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def run_id(self) -> str:
|
|
88
|
+
return self.info.run_id
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def status(self) -> str:
|
|
92
|
+
return self.info.status
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def done(self) -> bool:
|
|
96
|
+
return self.info.status not in {"queued", "running"}
|
|
97
|
+
|
|
98
|
+
def result(self) -> CallResult | SubmissionResult:
|
|
99
|
+
if not self.done:
|
|
100
|
+
raise ProtocolError("Job is still running; call wait() before result()")
|
|
101
|
+
if self.info.status != "succeeded":
|
|
102
|
+
error = self.info.error
|
|
103
|
+
raise api_error(
|
|
104
|
+
error.code if error else f"job_{self.info.status}",
|
|
105
|
+
error.message if error else f"Job {self.info.status}",
|
|
106
|
+
details=error.details if error else {},
|
|
107
|
+
job_id=self.job_id,
|
|
108
|
+
)
|
|
109
|
+
if self.info.result is None:
|
|
110
|
+
raise ProtocolError("Succeeded job is missing its result")
|
|
111
|
+
if self.info.kind == "submission":
|
|
112
|
+
return parse_model(SubmissionResult, self.info.result)
|
|
113
|
+
return parse_model(CallResult, self.info.result)
|