soliplex 0.83.4__tar.gz → 0.85__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.
- {soliplex-0.83.4/src/soliplex.egg-info → soliplex-0.85}/PKG-INFO +5 -4
- {soliplex-0.83.4 → soliplex-0.85}/pyproject.toml +5 -4
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agents.py +7 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/installation.py +7 -5
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/routing.py +15 -2
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/skills.py +70 -25
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/loggers.py +34 -48
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/models.py +1 -2
- soliplex-0.85/src/soliplex/sandbox_audit.py +145 -0
- soliplex-0.85/src/soliplex/skills/bwrap_sandbox/SKILL.md +79 -0
- soliplex-0.85/src/soliplex/skills/bwrap_sandbox/__init__.py +1112 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/sandbox_workdirs.py +32 -28
- {soliplex-0.83.4 → soliplex-0.85/src/soliplex.egg-info}/PKG-INFO +5 -4
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex.egg-info/requires.txt +4 -3
- soliplex-0.83.4/src/soliplex/sandbox_audit.py +0 -164
- soliplex-0.83.4/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -219
- soliplex-0.83.4/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -858
- {soliplex-0.83.4 → soliplex-0.85}/LICENSE +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/MANIFEST.in +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/README.md +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/example/skills/bare-bones/SKILL.md +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/example/skills/stanzas/SKILL.md +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/setup.cfg +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/skills/soliplex-docs/SKILL.md +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agui/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agui/parser.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agui/persistence.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agui/schema.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/agui/util.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/env.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/216d48e1e2e5_soliplex_v0_53.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/4b63e5f3f39e_soliplex_v0_51_1.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/63edaa5987f6_soliplex_v0_67.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/a1c7d3e90b42_soliplex_v0_80.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/b7e2f41c9d05_soliplex_v0_82.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/alembic_migrations/versions/d5009d4f9874_soliplex_v0_44.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/authn.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/authz/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/authz/persistence.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/authz/schema.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/capabilities/filesystem.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/capabilities/rag_audit.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/admin_users.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/ask.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/_common.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/admin_users.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/completions.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/databases.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/environment.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/installation.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/interpolation.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/logfire.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/logging.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/oidc.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/ollama.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/quizzes.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/room_authz.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/rooms.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/secrets.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/audit/skills.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/cli_util.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/database.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/main.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/ollama.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/room_authz.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/serve.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/cli/types.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/completions.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/_utils.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/agents.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/agui.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/authsystem.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/completions.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/exceptions.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/interpolation.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/logfire.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/meta.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/middleware.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/quizzes.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/rag.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/rooms.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/secrets.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/config/tools.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/examples.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/installation.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/log_ingest.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/main.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/mcp_auth.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/mcp_client.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/mcp_server.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/ollama.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/quizzes.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/rag_audit.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/secrets.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/titles.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tools/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tools/agui_run_feedback.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tools/file_uploads.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tools/rag.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tui/cli.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tui/main.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tui/rest_api.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/tui/serve.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/util.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/__init__.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/agui.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/authn.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/authz.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/completions.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/installation.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/log_ingest.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/quizzes.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/room_file_uploads.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/rooms.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/stats.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/streaming.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/templates/consent.html.mako +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex/views/thread_file_uploads.py +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex.egg-info/SOURCES.txt +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex.egg-info/dependency_links.txt +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex.egg-info/entry_points.txt +0 -0
- {soliplex-0.83.4 → soliplex-0.85}/src/soliplex.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soliplex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.85
|
|
4
4
|
Summary: An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface.
|
|
5
5
|
Author-email: Enfold <info@enfoldsystems.net>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,13 +13,14 @@ Requires-Dist: aiohttp<3.15,>=3.14.2
|
|
|
13
13
|
Requires-Dist: aiosqlite
|
|
14
14
|
Requires-Dist: alembic<1.19,>=1.18.4
|
|
15
15
|
Requires-Dist: authlib
|
|
16
|
-
Requires-Dist: bubble-sandbox<0.
|
|
16
|
+
Requires-Dist: bubble-sandbox<0.14,>=0.13.0
|
|
17
|
+
Requires-Dist: packaging
|
|
17
18
|
Requires-Dist: cryptography>=50.0.0
|
|
18
19
|
Requires-Dist: fastapi
|
|
19
20
|
Requires-Dist: fastmcp<5.0,>=4.0.2
|
|
20
21
|
Requires-Dist: fakeredis!=2.35.0
|
|
21
22
|
Requires-Dist: greenlet
|
|
22
|
-
Requires-Dist: haiku.rag-slim<0.
|
|
23
|
+
Requires-Dist: haiku.rag-slim<0.96,>=0.95.0
|
|
23
24
|
Requires-Dist: idna>=3.15
|
|
24
25
|
Requires-Dist: itsdangerous
|
|
25
26
|
Requires-Dist: joserfc<1.7,>=1.6.7
|
|
@@ -33,7 +34,7 @@ Requires-Dist: logfire[fastapi]
|
|
|
33
34
|
Requires-Dist: pyasn1>=0.6.4
|
|
34
35
|
Requires-Dist: pydantic-ai-slim[google]<3.0,>=2.40.0
|
|
35
36
|
Requires-Dist: pydantic-settings<2.15,>=2.14.2
|
|
36
|
-
Requires-Dist: pyjwt<2.
|
|
37
|
+
Requires-Dist: pyjwt<2.16,>=2.15.0
|
|
37
38
|
Requires-Dist: pymdown-extensions>=11.0.0
|
|
38
39
|
Requires-Dist: python-keycloak
|
|
39
40
|
Requires-Dist: skills-ref
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soliplex"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.85"
|
|
8
8
|
description = "An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface."
|
|
9
9
|
authors = [{ name = "Enfold", email = "info@enfoldsystems.net" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -19,13 +19,14 @@ dependencies = [
|
|
|
19
19
|
"aiosqlite",
|
|
20
20
|
"alembic >= 1.18.4, < 1.19",
|
|
21
21
|
"authlib",
|
|
22
|
-
"bubble-sandbox >= 0.
|
|
22
|
+
"bubble-sandbox >= 0.13.0, < 0.14",
|
|
23
|
+
"packaging",
|
|
23
24
|
"cryptography >= 50.0.0", # PYSEC-2026-3552
|
|
24
25
|
"fastapi",
|
|
25
26
|
"fastmcp >= 4.0.2, < 5.0",
|
|
26
27
|
"fakeredis != 2.35.0", # brownbag
|
|
27
28
|
"greenlet",
|
|
28
|
-
"haiku.rag-slim >= 0.
|
|
29
|
+
"haiku.rag-slim >= 0.95.0, < 0.96",
|
|
29
30
|
"idna >= 3.15",
|
|
30
31
|
"itsdangerous",
|
|
31
32
|
"joserfc >= 1.6.7, < 1.7", # CVE-2026-48990
|
|
@@ -39,7 +40,7 @@ dependencies = [
|
|
|
39
40
|
"pyasn1 >= 0.6.4", # PYSEC-2026-3456
|
|
40
41
|
"pydantic-ai-slim[google] >= 2.40.0, < 3.0",
|
|
41
42
|
"pydantic-settings >= 2.14.2, < 2.15", # GHSA-4xgf-cpjx-pc3j
|
|
42
|
-
"pyjwt >= 2.
|
|
43
|
+
"pyjwt >= 2.15.0, < 2.16", # CVE-2026-102275
|
|
43
44
|
"pymdown-extensions >= 11.0.0", # CVE-2026-61632
|
|
44
45
|
"python-keycloak",
|
|
45
46
|
"skills-ref",
|
|
@@ -17,6 +17,7 @@ from soliplex import models
|
|
|
17
17
|
from soliplex.capabilities import rag_audit as cap_rag_audit
|
|
18
18
|
from soliplex.config import agents as config_agents
|
|
19
19
|
from soliplex.config import tools as config_tools
|
|
20
|
+
from soliplex.skills import bwrap_sandbox
|
|
20
21
|
|
|
21
22
|
ToolConfigMap = dict[str, typing.Any]
|
|
22
23
|
|
|
@@ -138,6 +139,12 @@ def get_default_agent_from_configs(
|
|
|
138
139
|
if isinstance(capability, hr_caps_rag.RAGCapability):
|
|
139
140
|
capability.vision = agent_config.multimodal
|
|
140
141
|
|
|
142
|
+
# 'read_image' returns an image to the model, so a room whose model
|
|
143
|
+
# cannot accept one should not see the tool at all.
|
|
144
|
+
for capability in capabilities:
|
|
145
|
+
if isinstance(capability, bwrap_sandbox.SandboxCapability):
|
|
146
|
+
capability.multimodal = agent_config.multimodal
|
|
147
|
+
|
|
141
148
|
# A hook-only capability is never deferred: nothing would load it, and
|
|
142
149
|
# its hooks only fire once loaded.
|
|
143
150
|
for capability in capabilities:
|
|
@@ -315,13 +315,15 @@ class SandboxConfig:
|
|
|
315
315
|
return None
|
|
316
316
|
|
|
317
317
|
@property
|
|
318
|
-
def workdirs_path(self) -> pathlib.Path:
|
|
319
|
-
"""Directory holding
|
|
318
|
+
def workdirs_path(self) -> pathlib.Path | None:
|
|
319
|
+
"""Directory holding each thread's sandbox workspace
|
|
320
320
|
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
Named with the thread ID, under a directory for the room ID. The
|
|
322
|
+
workspace persists across the thread's turns, and the workdirs
|
|
323
|
+
endpoint serves the files at its top level.
|
|
323
324
|
|
|
324
|
-
If not set,
|
|
325
|
+
If not set, each command gets a temporary directory instead, which
|
|
326
|
+
is discarded when it ends and which the endpoint cannot serve.
|
|
325
327
|
"""
|
|
326
328
|
if self._config_path is not None and self._workdirs_path is not None:
|
|
327
329
|
return (self._config_path.parent / self._workdirs_path).resolve()
|
|
@@ -5,6 +5,7 @@ import pathlib
|
|
|
5
5
|
import typing
|
|
6
6
|
|
|
7
7
|
import fastapi
|
|
8
|
+
from bubble_sandbox import sandbox as bs_sandbox
|
|
8
9
|
|
|
9
10
|
from . import _utils
|
|
10
11
|
from . import exceptions as config_exc
|
|
@@ -230,7 +231,6 @@ _DEFAULT_ROUTER_NAMES = {
|
|
|
230
231
|
"completions": "soliplex.views.completions.router",
|
|
231
232
|
"room_file_uploads": "soliplex.views.room_file_uploads.router",
|
|
232
233
|
"thread_file_uploads": "soliplex.views.thread_file_uploads.router",
|
|
233
|
-
"sandbox_workdirs": "soliplex.views.sandbox_workdirs.router",
|
|
234
234
|
"installation": "soliplex.views.installation.router",
|
|
235
235
|
"log_ingest": "soliplex.views.log_ingest.router",
|
|
236
236
|
"quizzes": "soliplex.views.quizzes.router",
|
|
@@ -239,8 +239,21 @@ _DEFAULT_ROUTER_NAMES = {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
# Routers serving what the bubblewrap sandbox leaves behind, mounted only
|
|
243
|
+
# where 'bwrap' can run.
|
|
244
|
+
_BWRAP_ROUTER_NAMES = {
|
|
245
|
+
"sandbox_workdirs": "soliplex.views.sandbox_workdirs.router",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _default_router_names() -> dict[str, str]:
|
|
250
|
+
if bs_sandbox.is_available():
|
|
251
|
+
return _DEFAULT_ROUTER_NAMES | _BWRAP_ROUTER_NAMES
|
|
252
|
+
return dict(_DEFAULT_ROUTER_NAMES)
|
|
253
|
+
|
|
254
|
+
|
|
242
255
|
def register_default_routers():
|
|
243
|
-
for group_name, router_name in
|
|
256
|
+
for group_name, router_name in _default_router_names().items():
|
|
244
257
|
router = _utils._from_dotted_name(router_name)
|
|
245
258
|
APP_ROUTERS_BY_GROUP_NAME[group_name] = (
|
|
246
259
|
router,
|
|
@@ -10,6 +10,7 @@ import typing
|
|
|
10
10
|
import pydantic
|
|
11
11
|
from bubble_sandbox import config as bs_config
|
|
12
12
|
from bubble_sandbox import models as bs_models
|
|
13
|
+
from bubble_sandbox import sandbox as bs_sandbox
|
|
13
14
|
from haiku.rag.capabilities import compaction as hr_compaction
|
|
14
15
|
from haiku.rag.capabilities import policy as hr_policy
|
|
15
16
|
from haiku.rag.capabilities import rag as hr_rag
|
|
@@ -31,6 +32,11 @@ _default_list_field = _utils._default_list_field
|
|
|
31
32
|
_no_repr_no_compare_none = _utils._no_repr_no_compare_none
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
# Applied when the room sets no limit.
|
|
36
|
+
DEFAULT_EXECUTION_TIMEOUT_SECONDS = 30.0
|
|
37
|
+
DEFAULT_MAX_OUTPUT_CHARS = 10_000
|
|
38
|
+
|
|
39
|
+
|
|
34
40
|
class InvalidSkillKind(KeyError):
|
|
35
41
|
def __init__(
|
|
36
42
|
self,
|
|
@@ -49,6 +55,23 @@ class InvalidSkillKind(KeyError):
|
|
|
49
55
|
)
|
|
50
56
|
|
|
51
57
|
|
|
58
|
+
class BwrapSandboxUnavailable(RuntimeError):
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
skill_kind: str,
|
|
63
|
+
reason: bs_sandbox.SandboxUnavailable,
|
|
64
|
+
_config_path: pathlib.Path | None,
|
|
65
|
+
):
|
|
66
|
+
self.skill_kind = skill_kind
|
|
67
|
+
self.reason = reason
|
|
68
|
+
self._config_path = _config_path
|
|
69
|
+
super().__init__(
|
|
70
|
+
f"Skill kind '{skill_kind}' cannot run on this host: {reason} "
|
|
71
|
+
f"(configured in {_config_path})",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
52
75
|
class MissingSkillNames(KeyError):
|
|
53
76
|
def __init__(
|
|
54
77
|
self,
|
|
@@ -369,9 +392,10 @@ class BwrapSandboxSkillConfig:
|
|
|
369
392
|
_config_path: pathlib.Path | None = None
|
|
370
393
|
|
|
371
394
|
id: str | None = None
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
395
|
+
environment: str = "bare"
|
|
396
|
+
# Unset means "whatever the default is", not its value today.
|
|
397
|
+
execution_timeout_seconds: float | None = None
|
|
398
|
+
max_output_chars: int | None = None
|
|
375
399
|
volumes: bs_models.VolumeMap = _default_dict_field()
|
|
376
400
|
defer_loading: bool = False
|
|
377
401
|
|
|
@@ -386,10 +410,6 @@ class BwrapSandboxSkillConfig:
|
|
|
386
410
|
config_dict.pop("kind", None)
|
|
387
411
|
config_dict["_installation_config"] = installation_config
|
|
388
412
|
config_dict["_config_path"] = config_path
|
|
389
|
-
config_dict["sandbox_config"] = bs_config.Config(
|
|
390
|
-
config_file_path=config_path,
|
|
391
|
-
**config_dict.pop("sandbox_config", {}),
|
|
392
|
-
)
|
|
393
413
|
config_dict["volumes"] = {
|
|
394
414
|
key: bs_models.VolumeInfo(**value)
|
|
395
415
|
for key, value in config_dict.pop("volumes", {}).items()
|
|
@@ -402,12 +422,45 @@ class BwrapSandboxSkillConfig:
|
|
|
402
422
|
config_dict,
|
|
403
423
|
) from exc
|
|
404
424
|
|
|
425
|
+
@property
|
|
426
|
+
def sandbox_config(self) -> bs_config.Config:
|
|
427
|
+
timeout = self.execution_timeout_seconds
|
|
428
|
+
max_chars = self.max_output_chars
|
|
429
|
+
return bs_config.Config(
|
|
430
|
+
config_file_path=self._config_path,
|
|
431
|
+
execution_timeout_seconds=(
|
|
432
|
+
timeout
|
|
433
|
+
if timeout is not None
|
|
434
|
+
else DEFAULT_EXECUTION_TIMEOUT_SECONDS
|
|
435
|
+
),
|
|
436
|
+
max_output_chars=(
|
|
437
|
+
max_chars
|
|
438
|
+
if max_chars is not None
|
|
439
|
+
else DEFAULT_MAX_OUTPUT_CHARS
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
|
|
405
443
|
@property
|
|
406
444
|
def capability(self) -> bwrap_sandbox.SandboxCapability:
|
|
445
|
+
"""Build the sandbox capability.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
|
|
449
|
+
- 'BwrapSandboxUnavailable' on a host where 'bwrap' cannot run,
|
|
450
|
+
as reported by 'bubble_sandbox.sandbox.check_available'.
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
bs_sandbox.check_available()
|
|
454
|
+
except bs_sandbox.SandboxUnavailable as exc:
|
|
455
|
+
raise BwrapSandboxUnavailable(
|
|
456
|
+
skill_kind=self.kind,
|
|
457
|
+
reason=exc,
|
|
458
|
+
_config_path=self._config_path,
|
|
459
|
+
) from exc
|
|
460
|
+
|
|
407
461
|
return bwrap_sandbox.create_bwrap_sandbox_capability(
|
|
408
462
|
id=self.id,
|
|
409
|
-
|
|
410
|
-
allowed_environments=self.allowed_environments,
|
|
463
|
+
environment=self.environment,
|
|
411
464
|
sandbox_config=self.sandbox_config,
|
|
412
465
|
volumes=self.volumes,
|
|
413
466
|
installation_config=self._installation_config,
|
|
@@ -418,22 +471,17 @@ class BwrapSandboxSkillConfig:
|
|
|
418
471
|
def as_yaml(self) -> dict:
|
|
419
472
|
result = {
|
|
420
473
|
"kind": self.kind,
|
|
421
|
-
"
|
|
474
|
+
"environment": self.environment,
|
|
422
475
|
"defer_loading": self.defer_loading,
|
|
423
476
|
}
|
|
424
477
|
if self.id is not None:
|
|
425
478
|
result["id"] = self.id
|
|
426
|
-
if self.
|
|
427
|
-
result["
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"execution_timeout_seconds": (
|
|
433
|
-
config.execution_timeout_seconds
|
|
434
|
-
),
|
|
435
|
-
"max_output_chars": config.max_output_chars,
|
|
436
|
-
}
|
|
479
|
+
if self.execution_timeout_seconds is not None:
|
|
480
|
+
result["execution_timeout_seconds"] = (
|
|
481
|
+
self.execution_timeout_seconds
|
|
482
|
+
)
|
|
483
|
+
if self.max_output_chars is not None:
|
|
484
|
+
result["max_output_chars"] = self.max_output_chars
|
|
437
485
|
if self.volumes:
|
|
438
486
|
result["volumes"] = {
|
|
439
487
|
key: {
|
|
@@ -446,10 +494,7 @@ class BwrapSandboxSkillConfig:
|
|
|
446
494
|
|
|
447
495
|
@property
|
|
448
496
|
def extra_parameters(self) -> dict[str, typing.Any]:
|
|
449
|
-
|
|
450
|
-
if self.allowed_environments is not None:
|
|
451
|
-
result["allowed_environments"] = self.allowed_environments
|
|
452
|
-
return result
|
|
497
|
+
return {"environment": self.environment}
|
|
453
498
|
|
|
454
499
|
|
|
455
500
|
@dataclasses.dataclass(kw_only=True)
|
|
@@ -42,8 +42,8 @@ UPLOADS_GET_ROOM_THREAD_FILE = "uploads get room thread file"
|
|
|
42
42
|
UPLOADS_POST_ROOM = "uploads post room"
|
|
43
43
|
UPLOADS_POST_ROOM_THREAD = "uploads post room thread"
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
WORKDIRS_GET_ROOM_THREAD = "workdirs get room thread"
|
|
46
|
+
WORKDIRS_GET_ROOM_THREAD_FILE = "workdirs get room thread file"
|
|
47
47
|
|
|
48
48
|
AUTHN_BAD_REQUEST = "authn bad request"
|
|
49
49
|
AUTHN_CONSENT_CANCELLED = "unlisted frontend origin: sign-in cancelled"
|
|
@@ -186,22 +186,20 @@ AUDIT_RAG_ACTION_CHUNK_VIZ = "chunk-viz"
|
|
|
186
186
|
AUDIT_RAG_ACTION_DOC_LIST = "doc-list"
|
|
187
187
|
|
|
188
188
|
# sandbox-exec audit events (data change): the room agent's sandbox skill
|
|
189
|
-
# executes code against
|
|
189
|
+
# executes code against the thread's writable working directory, or reads
|
|
190
|
+
# an image out of a mount.
|
|
190
191
|
AUDIT_SANDBOX_EXEC = "sandbox exec"
|
|
191
192
|
|
|
192
|
-
# sandbox-exec 'action' values: which tool
|
|
193
|
+
# sandbox-exec 'action' values: which tool made the record.
|
|
193
194
|
AUDIT_SANDBOX_ACTION_RUN = "run"
|
|
194
195
|
AUDIT_SANDBOX_ACTION_RUN_PYTHON = "run-python"
|
|
196
|
+
AUDIT_SANDBOX_ACTION_READ_IMAGE = "read-image"
|
|
195
197
|
|
|
196
198
|
# sandbox-exec 'reason' values for a non-zero exit: the sandbox cut the
|
|
197
199
|
# execution off, or the code itself ended badly.
|
|
198
200
|
AUDIT_SANDBOX_REASON_TIMEOUT = "timeout"
|
|
199
201
|
AUDIT_SANDBOX_REASON_EXIT_CODE = "exit-code"
|
|
200
202
|
|
|
201
|
-
# sandbox volume-list audit events (disclosure): the sandbox skill tells
|
|
202
|
-
# the agent which uploaded files a volume holds.
|
|
203
|
-
AUDIT_SANDBOX_VOLUME_LIST = "sandbox volume list"
|
|
204
|
-
|
|
205
203
|
# room-upload audit events: an admin (privileged) adds shared reference
|
|
206
204
|
# material to a room, changing what the room's agent and members can access.
|
|
207
205
|
AUDIT_ROOM_UPLOAD_ADDED = "room upload added"
|
|
@@ -651,25 +649,11 @@ class RAGAccessAuditLog(AuditLogWrapper):
|
|
|
651
649
|
|
|
652
650
|
|
|
653
651
|
class SandboxExecAuditLog(AuditLogWrapper):
|
|
654
|
-
"""Record the sandbox skill's executions and
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
recorded inline (size / content leakage); the record captures the
|
|
660
|
-
'action' (which tool ran), the 'workdir' whose data the execution may
|
|
661
|
-
have changed, the 'environment' it ran in, 'refs' -- the host paths of
|
|
662
|
-
the saved command / script transcripts (empty when transcripts are not
|
|
663
|
-
configured) -- and the 'exit_code' the execution ended with.
|
|
664
|
-
|
|
665
|
-
An execution refused for naming an unavailable environment is recorded
|
|
666
|
-
as 'denied': nothing ran, so it carries no exit code, and its 'workdir'
|
|
667
|
-
is None because none was created.
|
|
668
|
-
|
|
669
|
-
``list_volume_files`` discloses the names of a volume's uploaded files
|
|
670
|
-
to the agent, so it is recorded too, as its own event carrying the
|
|
671
|
-
'volume' and the 'count' of files disclosed. The names themselves stay
|
|
672
|
-
out of the record, for the same reason the bodies do.
|
|
652
|
+
"""Record the sandbox skill's executions and image reads.
|
|
653
|
+
|
|
654
|
+
A record names the 'action', the 'workdir' the execution could change,
|
|
655
|
+
the 'environment' it ran in, 'refs' for the saved transcripts, and how
|
|
656
|
+
it ended. Command and script bodies are never recorded inline.
|
|
673
657
|
"""
|
|
674
658
|
|
|
675
659
|
def __init__(self, claims: dict[str, typing.Any], **extra):
|
|
@@ -686,6 +670,8 @@ class SandboxExecAuditLog(AuditLogWrapper):
|
|
|
686
670
|
refs: typing.Any,
|
|
687
671
|
*,
|
|
688
672
|
exit_code: int | None = None,
|
|
673
|
+
truncated: bool = False,
|
|
674
|
+
timeout_seconds: float | None = None,
|
|
689
675
|
):
|
|
690
676
|
self._succeeded(
|
|
691
677
|
AUDIT_SANDBOX_EXEC,
|
|
@@ -694,6 +680,8 @@ class SandboxExecAuditLog(AuditLogWrapper):
|
|
|
694
680
|
environment=environment,
|
|
695
681
|
refs=refs,
|
|
696
682
|
exit_code=exit_code,
|
|
683
|
+
truncated=truncated,
|
|
684
|
+
timeout_seconds=timeout_seconds,
|
|
697
685
|
)
|
|
698
686
|
|
|
699
687
|
def execute_failed(
|
|
@@ -705,6 +693,8 @@ class SandboxExecAuditLog(AuditLogWrapper):
|
|
|
705
693
|
reason: str,
|
|
706
694
|
*,
|
|
707
695
|
exit_code: int | None = None,
|
|
696
|
+
truncated: bool = False,
|
|
697
|
+
timeout_seconds: float | None = None,
|
|
708
698
|
):
|
|
709
699
|
self._failed(
|
|
710
700
|
AUDIT_SANDBOX_EXEC,
|
|
@@ -714,36 +704,32 @@ class SandboxExecAuditLog(AuditLogWrapper):
|
|
|
714
704
|
refs=refs,
|
|
715
705
|
reason=reason,
|
|
716
706
|
exit_code=exit_code,
|
|
707
|
+
truncated=truncated,
|
|
708
|
+
timeout_seconds=timeout_seconds,
|
|
717
709
|
)
|
|
718
710
|
|
|
719
|
-
def
|
|
711
|
+
def image_read(
|
|
720
712
|
self,
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
713
|
+
path: str,
|
|
714
|
+
volume: str,
|
|
715
|
+
*,
|
|
716
|
+
byte_count: int,
|
|
717
|
+
media_type: str,
|
|
726
718
|
):
|
|
727
|
-
self._denied(
|
|
728
|
-
AUDIT_SANDBOX_EXEC,
|
|
729
|
-
action=action,
|
|
730
|
-
workdir=workdir,
|
|
731
|
-
environment=environment,
|
|
732
|
-
refs=refs,
|
|
733
|
-
reason=reason,
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
def volume_listed(self, volume: str, count: int):
|
|
737
719
|
self._succeeded(
|
|
738
|
-
|
|
720
|
+
AUDIT_SANDBOX_EXEC,
|
|
721
|
+
action=AUDIT_SANDBOX_ACTION_READ_IMAGE,
|
|
722
|
+
path=path,
|
|
739
723
|
volume=volume,
|
|
740
|
-
|
|
724
|
+
byte_count=byte_count,
|
|
725
|
+
media_type=media_type,
|
|
741
726
|
)
|
|
742
727
|
|
|
743
|
-
def
|
|
728
|
+
def image_read_failed(self, path: str, reason: str):
|
|
744
729
|
self._failed(
|
|
745
|
-
|
|
746
|
-
|
|
730
|
+
AUDIT_SANDBOX_EXEC,
|
|
731
|
+
action=AUDIT_SANDBOX_ACTION_READ_IMAGE,
|
|
732
|
+
path=path,
|
|
747
733
|
reason=reason,
|
|
748
734
|
)
|
|
749
735
|
|
|
@@ -665,10 +665,9 @@ class WorkdirFile(pydantic.BaseModel):
|
|
|
665
665
|
url: pydantic.HttpUrl
|
|
666
666
|
|
|
667
667
|
|
|
668
|
-
class
|
|
668
|
+
class ThreadWorkdirFiles(pydantic.BaseModel):
|
|
669
669
|
room_id: str
|
|
670
670
|
thread_id: str
|
|
671
|
-
run_id: str
|
|
672
671
|
files: list[WorkdirFile]
|
|
673
672
|
|
|
674
673
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import pathlib
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from soliplex import loggers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _SandboxExecRecorder:
|
|
9
|
+
"""Carries what an execution reports back to the surrounding audit."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.workdir: pathlib.Path | None = None
|
|
13
|
+
self.refs: list[str] = []
|
|
14
|
+
self.exit_code: int | None = None
|
|
15
|
+
self.timed_out: bool = False
|
|
16
|
+
self.timeout_seconds: float | None = None
|
|
17
|
+
self.truncated: bool = False
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def logged_workdir(self) -> str | None:
|
|
21
|
+
return str(self.workdir) if self.workdir is not None else None
|
|
22
|
+
|
|
23
|
+
def record_workdir(self, workdir: pathlib.Path | None):
|
|
24
|
+
self.workdir = workdir
|
|
25
|
+
|
|
26
|
+
def record_ref(self, ref: str):
|
|
27
|
+
self.refs.append(ref)
|
|
28
|
+
|
|
29
|
+
def record_result(self, result):
|
|
30
|
+
self.exit_code = result.exit_code
|
|
31
|
+
self.timed_out = result.timed_out
|
|
32
|
+
self.timeout_seconds = result.timeout_seconds
|
|
33
|
+
self.truncated = result.truncated
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _SandboxImageReadRecorder:
|
|
37
|
+
"""Carries what a read disclosed: which mount, how much, what type."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self.volume: str = ""
|
|
41
|
+
self.byte_count: int = 0
|
|
42
|
+
self.media_type: str = ""
|
|
43
|
+
|
|
44
|
+
def record_image(self, volume: str, data: bytes, media_type: str):
|
|
45
|
+
self.volume = volume
|
|
46
|
+
self.byte_count = len(data)
|
|
47
|
+
self.media_type = media_type
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _audit_log(deps: typing.Any) -> loggers.SandboxExecAuditLog:
|
|
51
|
+
"""Bind actor identity and run correlation from the agent deps."""
|
|
52
|
+
user = getattr(deps, "user", None)
|
|
53
|
+
|
|
54
|
+
return loggers.SandboxExecAuditLog(
|
|
55
|
+
claims=user.model_dump() if user is not None else {},
|
|
56
|
+
room_id=deps.room_id,
|
|
57
|
+
thread_id=deps.thread_id,
|
|
58
|
+
run_id=deps.run_id,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@contextlib.contextmanager
|
|
63
|
+
def audit_sandbox_exec(
|
|
64
|
+
deps: typing.Any,
|
|
65
|
+
*,
|
|
66
|
+
action: str,
|
|
67
|
+
environment: str | None,
|
|
68
|
+
):
|
|
69
|
+
"""Bracket a ``run`` / ``run_python`` tool body, emitting one
|
|
70
|
+
``sandbox-exec`` record.
|
|
71
|
+
|
|
72
|
+
Yields a recorder the tool feeds with the working directory, each
|
|
73
|
+
transcript path, and the result. The command and script bodies are
|
|
74
|
+
never logged.
|
|
75
|
+
"""
|
|
76
|
+
audit = _audit_log(deps)
|
|
77
|
+
recorder = _SandboxExecRecorder()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
yield recorder
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
audit.execute_failed(
|
|
83
|
+
action,
|
|
84
|
+
recorder.logged_workdir,
|
|
85
|
+
environment,
|
|
86
|
+
recorder.refs,
|
|
87
|
+
type(exc).__name__,
|
|
88
|
+
exit_code=recorder.exit_code,
|
|
89
|
+
truncated=recorder.truncated,
|
|
90
|
+
timeout_seconds=recorder.timeout_seconds,
|
|
91
|
+
)
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
bounds = {
|
|
95
|
+
"exit_code": recorder.exit_code,
|
|
96
|
+
"truncated": recorder.truncated,
|
|
97
|
+
"timeout_seconds": recorder.timeout_seconds,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if recorder.timed_out:
|
|
101
|
+
audit.execute_failed(
|
|
102
|
+
action,
|
|
103
|
+
recorder.logged_workdir,
|
|
104
|
+
environment,
|
|
105
|
+
recorder.refs,
|
|
106
|
+
loggers.AUDIT_SANDBOX_REASON_TIMEOUT,
|
|
107
|
+
**bounds,
|
|
108
|
+
)
|
|
109
|
+
elif recorder.exit_code: # neither a clean zero nor an absent status
|
|
110
|
+
audit.execute_failed(
|
|
111
|
+
action,
|
|
112
|
+
recorder.logged_workdir,
|
|
113
|
+
environment,
|
|
114
|
+
recorder.refs,
|
|
115
|
+
loggers.AUDIT_SANDBOX_REASON_EXIT_CODE,
|
|
116
|
+
**bounds,
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
audit.executed(
|
|
120
|
+
action,
|
|
121
|
+
recorder.logged_workdir,
|
|
122
|
+
environment,
|
|
123
|
+
recorder.refs,
|
|
124
|
+
**bounds,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@contextlib.contextmanager
|
|
129
|
+
def audit_sandbox_read_image(deps: typing.Any, *, path: str):
|
|
130
|
+
"""Bracket the ``read_image`` tool body, emitting one record."""
|
|
131
|
+
audit = _audit_log(deps)
|
|
132
|
+
recorder = _SandboxImageReadRecorder()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
yield recorder
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
audit.image_read_failed(path, type(exc).__name__)
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
audit.image_read(
|
|
141
|
+
path,
|
|
142
|
+
recorder.volume,
|
|
143
|
+
byte_count=recorder.byte_count,
|
|
144
|
+
media_type=recorder.media_type,
|
|
145
|
+
)
|