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.
@@ -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)