soliplex 0.82__tar.gz → 0.83__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.82/src/soliplex.egg-info → soliplex-0.83}/PKG-INFO +6 -2
- {soliplex-0.82 → soliplex-0.83}/pyproject.toml +21 -6
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agents.py +2 -8
- soliplex-0.83/src/soliplex/cli/audit/__init__.py +128 -0
- soliplex-0.83/src/soliplex/cli/audit/_common.py +89 -0
- soliplex-0.83/src/soliplex/cli/audit/admin_users.py +142 -0
- soliplex-0.83/src/soliplex/cli/audit/completions.py +74 -0
- soliplex-0.83/src/soliplex/cli/audit/databases.py +329 -0
- soliplex-0.83/src/soliplex/cli/audit/environment.py +77 -0
- soliplex-0.83/src/soliplex/cli/audit/installation.py +111 -0
- soliplex-0.83/src/soliplex/cli/audit/interpolation.py +525 -0
- soliplex-0.83/src/soliplex/cli/audit/logfire.py +45 -0
- soliplex-0.83/src/soliplex/cli/audit/logging.py +76 -0
- soliplex-0.83/src/soliplex/cli/audit/oidc.py +67 -0
- soliplex-0.83/src/soliplex/cli/audit/ollama.py +178 -0
- soliplex-0.83/src/soliplex/cli/audit/quizzes.py +80 -0
- soliplex-0.83/src/soliplex/cli/audit/room_authz.py +197 -0
- soliplex-0.83/src/soliplex/cli/audit/rooms.py +242 -0
- soliplex-0.83/src/soliplex/cli/audit/secrets.py +53 -0
- soliplex-0.83/src/soliplex/cli/audit/skills.py +134 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/cli_util.py +26 -3
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/database.py +1 -16
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/main.py +30 -19
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/agents.py +2 -2
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/rooms.py +4 -1
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/skills.py +12 -42
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/installation.py +29 -9
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/loggers.py +3 -0
- {soliplex-0.82 → soliplex-0.83/src/soliplex.egg-info}/PKG-INFO +6 -2
- {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/SOURCES.txt +17 -1
- {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/requires.txt +7 -1
- soliplex-0.82/src/soliplex/cli/audit.py +0 -2234
- {soliplex-0.82 → soliplex-0.83}/LICENSE +0 -0
- {soliplex-0.82 → soliplex-0.83}/MANIFEST.in +0 -0
- {soliplex-0.82 → soliplex-0.83}/README.md +0 -0
- {soliplex-0.82 → soliplex-0.83}/example/skills/bare-bones/SKILL.md +0 -0
- {soliplex-0.82 → soliplex-0.83}/example/skills/stanzas/SKILL.md +0 -0
- {soliplex-0.82 → soliplex-0.83}/setup.cfg +0 -0
- {soliplex-0.82 → soliplex-0.83}/skills/soliplex-docs/SKILL.md +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/parser.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/persistence.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/schema.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/util.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/env.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/216d48e1e2e5_soliplex_v0_53.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/4b63e5f3f39e_soliplex_v0_51_1.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/63edaa5987f6_soliplex_v0_67.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/a1c7d3e90b42_soliplex_v0_80.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/b7e2f41c9d05_soliplex_v0_82.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/d5009d4f9874_soliplex_v0_44.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/authn.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/persistence.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/schema.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/capabilities/filesystem.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/capabilities/rag_audit.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/admin_users.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/ask.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/ollama.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/room_authz.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/serve.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/types.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/completions.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/_utils.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/agui.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/authsystem.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/completions.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/exceptions.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/installation.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/interpolation.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/logfire.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/meta.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/middleware.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/quizzes.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/rag.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/routing.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/secrets.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/tools.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/examples.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/log_ingest.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/main.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_auth.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_client.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_server.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/models.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/ollama.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/quizzes.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/rag_audit.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/sandbox_audit.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/secrets.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/titles.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/agui_run_feedback.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/file_uploads.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/rag.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/cli.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/main.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/rest_api.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/serve.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/util.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/__init__.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/agui.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/authn.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/authz.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/completions.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/installation.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/log_ingest.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/quizzes.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/room_file_uploads.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/rooms.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/sandbox_workdirs.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/stats.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/streaming.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/thread_file_uploads.py +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/dependency_links.txt +0 -0
- {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/entry_points.txt +0 -0
- {soliplex-0.82 → soliplex-0.83}/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.83
|
|
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
|
|
@@ -19,7 +19,7 @@ Requires-Dist: fastapi
|
|
|
19
19
|
Requires-Dist: fastmcp<5.0,>=4.0.2
|
|
20
20
|
Requires-Dist: fakeredis!=2.35.0
|
|
21
21
|
Requires-Dist: greenlet
|
|
22
|
-
Requires-Dist: haiku.rag-slim<0.
|
|
22
|
+
Requires-Dist: haiku.rag-slim<0.88,>=0.87.0
|
|
23
23
|
Requires-Dist: idna>=3.15
|
|
24
24
|
Requires-Dist: itsdangerous
|
|
25
25
|
Requires-Dist: joserfc<1.7,>=1.6.7
|
|
@@ -41,6 +41,10 @@ Requires-Dist: sqlmodel
|
|
|
41
41
|
Requires-Dist: starlette>=1.3.1
|
|
42
42
|
Requires-Dist: uvicorn[standard]
|
|
43
43
|
Requires-Dist: certifi>=2025.11.12
|
|
44
|
+
Provides-Extra: postgres
|
|
45
|
+
Requires-Dist: psycopg; extra == "postgres"
|
|
46
|
+
Provides-Extra: postgres-binary
|
|
47
|
+
Requires-Dist: psycopg[binary]; extra == "postgres-binary"
|
|
44
48
|
Dynamic: license-file
|
|
45
49
|
|
|
46
50
|
# Soliplex
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "soliplex"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.83"
|
|
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"
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
"fastmcp >= 4.0.2, < 5.0",
|
|
26
26
|
"fakeredis != 2.35.0", # brownbag
|
|
27
27
|
"greenlet",
|
|
28
|
-
"haiku.rag-slim >= 0.
|
|
28
|
+
"haiku.rag-slim >= 0.87.0, < 0.88",
|
|
29
29
|
"idna >= 3.15",
|
|
30
30
|
"itsdangerous",
|
|
31
31
|
"joserfc >= 1.6.7, < 1.7", # CVE-2026-48990
|
|
@@ -49,6 +49,24 @@ dependencies = [
|
|
|
49
49
|
"certifi >= 2025.11.12",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
# Database drivers are not dependencies: soliplex imports none of them,
|
|
53
|
+
# and SQLAlchemy loads one only when a DBURI names it. An installation
|
|
54
|
+
# using PostgreSQL picks one of these (see 'docs/config/dburis.md').
|
|
55
|
+
# psycopg serves both the sync and the async DBURIs
|
|
56
|
+
# ('postgresql+psycopg://').
|
|
57
|
+
[project.optional-dependencies]
|
|
58
|
+
# psycopg's pure-Python implementation, over the host's own libpq (and so
|
|
59
|
+
# the host's OpenSSL): for containers and FIPS hosts. Needs 'libpq5' (or
|
|
60
|
+
# equivalent) installed; no compiler.
|
|
61
|
+
postgres = [
|
|
62
|
+
"psycopg",
|
|
63
|
+
]
|
|
64
|
+
# psycopg with its own bundled libpq and OpenSSL: nothing needed on the
|
|
65
|
+
# host, e.g. for development.
|
|
66
|
+
postgres-binary = [
|
|
67
|
+
"psycopg[binary]",
|
|
68
|
+
]
|
|
69
|
+
|
|
52
70
|
[project.scripts]
|
|
53
71
|
soliplex-cli = "soliplex.cli.main:main"
|
|
54
72
|
soliplex-tui = "soliplex.tui.cli:the_cli"
|
|
@@ -68,10 +86,6 @@ dev = [
|
|
|
68
86
|
docs = [
|
|
69
87
|
"zensical>=0.0.43",
|
|
70
88
|
]
|
|
71
|
-
postgres = [
|
|
72
|
-
"psycopg[binary]", # synchronous access
|
|
73
|
-
"asyncpg", # async access
|
|
74
|
-
]
|
|
75
89
|
tui = [
|
|
76
90
|
"textual",
|
|
77
91
|
"textual-serve",
|
|
@@ -103,6 +117,7 @@ omit = [
|
|
|
103
117
|
"scripts/lint_textio.py", # runs its own '--self-test' during lint
|
|
104
118
|
"scripts/lint_interpolation.py", # ditto
|
|
105
119
|
"scripts/lint_alembic_chain.py", # ditto
|
|
120
|
+
"scripts/lint_postgres_drivers.py", # ditto
|
|
106
121
|
# Run-once deployment repair (issue #1367), applied by hand against a
|
|
107
122
|
# live database and verified there; like a migration script, it is not
|
|
108
123
|
# exercised by the unit suite.
|
|
@@ -4,7 +4,6 @@ import dataclasses
|
|
|
4
4
|
import typing
|
|
5
5
|
|
|
6
6
|
import pydantic_ai
|
|
7
|
-
from haiku.rag.capabilities import analysis as hr_caps_analysis
|
|
8
7
|
from haiku.rag.capabilities import rag as hr_caps_rag
|
|
9
8
|
from pydantic_ai import agent as ai_agent
|
|
10
9
|
from pydantic_ai import capabilities as ai_capabilities
|
|
@@ -21,11 +20,6 @@ from soliplex.config import tools as config_tools
|
|
|
21
20
|
|
|
22
21
|
ToolConfigMap = dict[str, typing.Any]
|
|
23
22
|
|
|
24
|
-
# Capabilities which take 'vision' from agent config's 'multimodal'
|
|
25
|
-
HR_VisionCapabilities = (
|
|
26
|
-
hr_caps_rag.RAGCapability | hr_caps_analysis.AnalysisCapability
|
|
27
|
-
)
|
|
28
|
-
|
|
29
23
|
|
|
30
24
|
class CapabilityConfig(typing.Protocol):
|
|
31
25
|
"""Room-specific native capabilities and their audit targets."""
|
|
@@ -136,12 +130,12 @@ def get_default_agent_from_configs(
|
|
|
136
130
|
)
|
|
137
131
|
)
|
|
138
132
|
|
|
139
|
-
# RAG
|
|
133
|
+
# The RAG capability attaches picture chunks to search results as
|
|
140
134
|
# images only when the receiving model accepts them. That model is this
|
|
141
135
|
# agent's model, not haiku.rag's configured model, so gate on the room
|
|
142
136
|
# agent's declared multimodality.
|
|
143
137
|
for capability in capabilities:
|
|
144
|
-
if isinstance(capability,
|
|
138
|
+
if isinstance(capability, hr_caps_rag.RAGCapability):
|
|
145
139
|
capability.vision = agent_config.multimodal
|
|
146
140
|
|
|
147
141
|
# A hook-only capability is never deferred: nothing would load it, and
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import core as typer_core
|
|
7
|
+
|
|
8
|
+
from soliplex.cli import cli_util
|
|
9
|
+
from soliplex.cli import types
|
|
10
|
+
from soliplex.cli.audit import _common as audit_common
|
|
11
|
+
from soliplex.cli.audit import admin_users as audit_admin_users
|
|
12
|
+
from soliplex.cli.audit import completions as audit_completions
|
|
13
|
+
from soliplex.cli.audit import databases as audit_databases
|
|
14
|
+
from soliplex.cli.audit import environment as audit_environment
|
|
15
|
+
from soliplex.cli.audit import installation as audit_installation
|
|
16
|
+
from soliplex.cli.audit import logfire as audit_logfire
|
|
17
|
+
from soliplex.cli.audit import logging as audit_logging
|
|
18
|
+
from soliplex.cli.audit import oidc as audit_oidc
|
|
19
|
+
from soliplex.cli.audit import ollama as audit_ollama
|
|
20
|
+
from soliplex.cli.audit import quizzes as audit_quizzes
|
|
21
|
+
from soliplex.cli.audit import room_authz as audit_room_authz
|
|
22
|
+
from soliplex.cli.audit import rooms as audit_rooms
|
|
23
|
+
from soliplex.cli.audit import secrets as audit_secrets
|
|
24
|
+
from soliplex.cli.audit import skills as audit_skills
|
|
25
|
+
|
|
26
|
+
AUDIT_HELP = "Audit a Soliplex installation configuration"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_QUIET_OPTION = typer.Option(
|
|
30
|
+
False,
|
|
31
|
+
"-q",
|
|
32
|
+
"--quiet",
|
|
33
|
+
help="Show only errors",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _AuditGroup(typer_core.TyperGroup):
|
|
38
|
+
"""Default to the 'all' subcommand when none is given.
|
|
39
|
+
|
|
40
|
+
Allows 'soliplex-cli audit', 'soliplex-cli audit -q', and
|
|
41
|
+
'soliplex-cli audit <path>' as shorthands for the corresponding
|
|
42
|
+
'soliplex-cli audit all ...' invocation. The path-less forms rely
|
|
43
|
+
on Typer's ``SOLIPLEX_INSTALLATION_PATH`` envvar fallback on
|
|
44
|
+
'audit all'.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def parse_args(self, ctx, args):
|
|
48
|
+
for i, token in enumerate(args):
|
|
49
|
+
if token.startswith("-"):
|
|
50
|
+
continue
|
|
51
|
+
if token not in self.commands:
|
|
52
|
+
args = [*args[:i], "all", *args[i:]]
|
|
53
|
+
break
|
|
54
|
+
else:
|
|
55
|
+
args = [*args, "all"]
|
|
56
|
+
return super().parse_args(ctx, args)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
app = typer.Typer(
|
|
60
|
+
name="audit",
|
|
61
|
+
help=AUDIT_HELP,
|
|
62
|
+
cls=_AuditGroup,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.callback()
|
|
67
|
+
def _audit_callback(
|
|
68
|
+
ctx: typer.Context,
|
|
69
|
+
quiet: bool = _QUIET_OPTION,
|
|
70
|
+
cli_log_config: pathlib.Path | None = cli_util.CLI_LOG_CONFIG_OPTION,
|
|
71
|
+
):
|
|
72
|
+
cli_util._configure_cli_logging(cli_log_config)
|
|
73
|
+
ctx.obj = {"quiet": quiet}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command(
|
|
77
|
+
"all",
|
|
78
|
+
help=AUDIT_HELP,
|
|
79
|
+
)
|
|
80
|
+
def audit_all(
|
|
81
|
+
ctx: typer.Context,
|
|
82
|
+
installation_path: types.installation_path_type,
|
|
83
|
+
):
|
|
84
|
+
quiet = ctx.obj["quiet"]
|
|
85
|
+
errors: dict = {}
|
|
86
|
+
|
|
87
|
+
errors |= audit_installation._audit_installation_section(
|
|
88
|
+
ctx, installation_path
|
|
89
|
+
)
|
|
90
|
+
errors |= audit_secrets._audit_secrets_section(ctx, installation_path)
|
|
91
|
+
errors |= audit_environment._audit_environment_section(
|
|
92
|
+
ctx, installation_path
|
|
93
|
+
)
|
|
94
|
+
errors |= audit_oidc._audit_oidc_section(ctx, installation_path)
|
|
95
|
+
errors |= audit_rooms._audit_rooms_section(ctx, installation_path)
|
|
96
|
+
errors |= audit_databases._audit_databases_section(ctx, installation_path)
|
|
97
|
+
errors |= audit_admin_users._audit_admin_users_section(
|
|
98
|
+
ctx, installation_path
|
|
99
|
+
)
|
|
100
|
+
errors |= audit_room_authz._audit_room_authz_section(
|
|
101
|
+
ctx, installation_path
|
|
102
|
+
)
|
|
103
|
+
errors |= audit_completions._audit_completions_section(
|
|
104
|
+
ctx, installation_path
|
|
105
|
+
)
|
|
106
|
+
errors |= audit_quizzes._audit_quizzes_section(ctx, installation_path)
|
|
107
|
+
errors |= audit_skills._audit_skills_section(ctx, installation_path)
|
|
108
|
+
errors |= audit_logging._audit_logging_section(ctx, installation_path)
|
|
109
|
+
errors |= audit_logfire._audit_logfire_section(ctx, installation_path)
|
|
110
|
+
errors |= audit_ollama._audit_ollama_section(ctx, installation_path)
|
|
111
|
+
|
|
112
|
+
audit_common._emit_errors(errors, quiet)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
app.command("installation")(audit_installation.audit_installation)
|
|
116
|
+
app.command("secrets")(audit_secrets.audit_secrets)
|
|
117
|
+
app.command("environment")(audit_environment.audit_environment)
|
|
118
|
+
app.command("oidc")(audit_oidc.audit_oidc_auth_providers)
|
|
119
|
+
app.command("rooms")(audit_rooms.audit_rooms)
|
|
120
|
+
app.command("databases")(audit_databases.audit_databases)
|
|
121
|
+
app.command("admin-users")(audit_admin_users.audit_admin_users)
|
|
122
|
+
app.command("room-authz")(audit_room_authz.audit_room_authz)
|
|
123
|
+
app.command("completions")(audit_completions.audit_completions)
|
|
124
|
+
app.command("quizzes")(audit_quizzes.audit_quizzes)
|
|
125
|
+
app.command("skills")(audit_skills.audit_skills)
|
|
126
|
+
app.command("logging")(audit_logging.audit_logging)
|
|
127
|
+
app.command("logfire")(audit_logfire.audit_logfire)
|
|
128
|
+
app.command("ollama")(audit_ollama.audit_ollama)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from soliplex import installation
|
|
9
|
+
from soliplex.cli import cli_util
|
|
10
|
+
from soliplex.cli import types
|
|
11
|
+
from soliplex.config import installation as config_installation
|
|
12
|
+
|
|
13
|
+
the_console = cli_util.the_console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _noop(*args, **kwargs): # pragma: NO COVER
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _quiet_console_funcs(quiet):
|
|
21
|
+
"""Return ``(line, rule, print, print_exception)`` callables.
|
|
22
|
+
|
|
23
|
+
When ``quiet`` is true the returned callables are no-ops, suppressing
|
|
24
|
+
human-focused output.
|
|
25
|
+
"""
|
|
26
|
+
if quiet:
|
|
27
|
+
return _noop, _noop, _noop, _noop
|
|
28
|
+
return (
|
|
29
|
+
the_console.line,
|
|
30
|
+
the_console.rule,
|
|
31
|
+
the_console.print,
|
|
32
|
+
the_console.print_exception,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _emit_errors(errors, quiet):
|
|
37
|
+
"""Emit a JSON error report (in quiet mode) and exit ``1`` if any."""
|
|
38
|
+
if errors:
|
|
39
|
+
if quiet:
|
|
40
|
+
the_console.print_json(data=errors)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_installation(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
installation_path: types.installation_path_type,
|
|
47
|
+
) -> installation.Installation:
|
|
48
|
+
"""Load the installation once per invocation, caching on ``ctx.obj``.
|
|
49
|
+
|
|
50
|
+
Every warning raised while loading is recorded under
|
|
51
|
+
``ctx.obj["config_warnings"]``.
|
|
52
|
+
"""
|
|
53
|
+
cached = ctx.obj.get("the_installation")
|
|
54
|
+
if cached is None:
|
|
55
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
56
|
+
warnings.simplefilter("always")
|
|
57
|
+
cached = cli_util.get_installation(
|
|
58
|
+
installation_path, auditing=True
|
|
59
|
+
)
|
|
60
|
+
ctx.obj["the_installation"] = cached
|
|
61
|
+
ctx.obj["config_warnings"] = caught
|
|
62
|
+
return cached
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_installation_config(
|
|
66
|
+
ctx: typer.Context,
|
|
67
|
+
installation_path: types.installation_path_type,
|
|
68
|
+
) -> config_installation.InstallationConfig:
|
|
69
|
+
"""Load only the installation config once, caching on ``ctx.obj``.
|
|
70
|
+
|
|
71
|
+
Unlike '_get_installation', loads no OIDC, room, completion, quiz or
|
|
72
|
+
skill configs. As there, missing environment variables are
|
|
73
|
+
tolerated, and every warning raised while loading is recorded under
|
|
74
|
+
``ctx.obj["config_warnings"]``.
|
|
75
|
+
"""
|
|
76
|
+
cached = ctx.obj.get("the_installation_config")
|
|
77
|
+
if cached is None:
|
|
78
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
79
|
+
warnings.simplefilter("always")
|
|
80
|
+
cached = config_installation.load_installation(
|
|
81
|
+
cli_util.installation_config_path(installation_path)
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
cached.resolve_environment()
|
|
85
|
+
except config_installation.MissingEnvVars:
|
|
86
|
+
pass
|
|
87
|
+
ctx.obj["the_installation_config"] = cached
|
|
88
|
+
ctx.obj["config_warnings"] = caught
|
|
89
|
+
return cached
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from soliplex import authz
|
|
8
|
+
from soliplex.cli import cli_util
|
|
9
|
+
from soliplex.cli import types
|
|
10
|
+
from soliplex.cli.audit import _common as audit_common
|
|
11
|
+
from soliplex.cli.audit import databases as audit_databases
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def _list_admin_discriminators(the_installation):
|
|
15
|
+
async with cli_util._admin_user_policy(
|
|
16
|
+
the_installation, "audit admin-users", allow_ram=True, must_exist=True
|
|
17
|
+
) as policy:
|
|
18
|
+
return await policy.list_admin_user_discriminators()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _admin_user_json_paths(
|
|
22
|
+
the_installation,
|
|
23
|
+
) -> tuple[list[str], str | None]:
|
|
24
|
+
"""Return ``(json_paths, error)`` for the stored admin rows.
|
|
25
|
+
|
|
26
|
+
``json_paths`` holds every stored 'AdminUser.json_path', in insertion
|
|
27
|
+
order, read via 'AdminUserPolicy.list_admin_user_discriminators';
|
|
28
|
+
``error`` is ``None`` on success. A database nothing has created yet
|
|
29
|
+
-- including an in-memory one -- holds no admin rows, so it reports
|
|
30
|
+
``([], None)`` rather than creating a schema to read from.
|
|
31
|
+
|
|
32
|
+
When the database itself cannot be reached -- e.g. its DBURI names a
|
|
33
|
+
Postgres server that isn't listening -- ``json_paths`` is empty and
|
|
34
|
+
``error`` carries the exception message, so the audit can report the
|
|
35
|
+
unreachable DB instead of dying on the traceback.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return (
|
|
39
|
+
list(asyncio.run(_list_admin_discriminators(the_installation))),
|
|
40
|
+
None,
|
|
41
|
+
)
|
|
42
|
+
except cli_util.DatabaseNotCreated:
|
|
43
|
+
# As above: no schema means nothing to audit.
|
|
44
|
+
return [], None
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
return [], f"{type(exc).__name__}: {exc}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _invalid_admin_user_json_paths(
|
|
50
|
+
json_paths,
|
|
51
|
+
) -> list[tuple[str, str]]:
|
|
52
|
+
"""Collect admin entries whose 'json_path' fails to validate.
|
|
53
|
+
|
|
54
|
+
Same intent as '_invalid_acl_json_paths' but for the 'AdminUser'
|
|
55
|
+
table: a row typically lands here when it was authored under a
|
|
56
|
+
meta-config that registered filter functions which are no longer
|
|
57
|
+
present. ``json_paths`` is the list from '_admin_user_json_paths'.
|
|
58
|
+
|
|
59
|
+
Returns a list of '(json_path, error)' pairs.
|
|
60
|
+
"""
|
|
61
|
+
invalid: list[tuple[str, str]] = []
|
|
62
|
+
for json_path in json_paths:
|
|
63
|
+
try:
|
|
64
|
+
authz.validate_json_path(json_path)
|
|
65
|
+
except authz.InvalidJSONPath as exc:
|
|
66
|
+
invalid.append((json_path, str(exc)))
|
|
67
|
+
return invalid
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _audit_admin_users_section(
|
|
71
|
+
ctx: typer.Context,
|
|
72
|
+
installation_path: types.installation_path_type,
|
|
73
|
+
) -> dict: # pragma NO COVER UI ONLY
|
|
74
|
+
"""Print the admin-users section (rule header + listing + invalid)."""
|
|
75
|
+
quiet = ctx.obj["quiet"]
|
|
76
|
+
the_installation = audit_common._get_installation(ctx, installation_path)
|
|
77
|
+
tc_line, tc_rule, tc_print, _ = audit_common._quiet_console_funcs(quiet)
|
|
78
|
+
|
|
79
|
+
tc_line()
|
|
80
|
+
tc_rule("Configured admin users")
|
|
81
|
+
tc_line()
|
|
82
|
+
|
|
83
|
+
report = audit_databases._database_reports(ctx, the_installation._config)[
|
|
84
|
+
cli_util.AUTHZ
|
|
85
|
+
]
|
|
86
|
+
if report.error is not None:
|
|
87
|
+
if ctx.obj.get("databases_audited"):
|
|
88
|
+
tc_print(audit_databases._SEE_DATABASES)
|
|
89
|
+
tc_line()
|
|
90
|
+
return {}
|
|
91
|
+
tc_print(f"ERROR: authorization database unreachable: {report.error}")
|
|
92
|
+
tc_line()
|
|
93
|
+
return {"admin_users": {"unreachable": report.error}}
|
|
94
|
+
|
|
95
|
+
json_paths, db_error = _admin_user_json_paths(the_installation)
|
|
96
|
+
if db_error is not None:
|
|
97
|
+
tc_print(f"ERROR: authorization database unreachable: {db_error}")
|
|
98
|
+
tc_line()
|
|
99
|
+
return {"admin_users": {"unreachable": db_error}}
|
|
100
|
+
|
|
101
|
+
tc_print(f"Admin users ({len(json_paths)}):")
|
|
102
|
+
if json_paths:
|
|
103
|
+
for json_path in json_paths:
|
|
104
|
+
parsed = authz.parse_token_field_json_path(json_path)
|
|
105
|
+
if parsed is not None:
|
|
106
|
+
field, value = parsed
|
|
107
|
+
tc_print(f" - {field}={value}")
|
|
108
|
+
else:
|
|
109
|
+
tc_print(f" - json_path={json_path}")
|
|
110
|
+
else:
|
|
111
|
+
tc_print(" (none)")
|
|
112
|
+
tc_line()
|
|
113
|
+
|
|
114
|
+
invalid = _invalid_admin_user_json_paths(json_paths)
|
|
115
|
+
tc_print("Admin users with invalid JSONPath:")
|
|
116
|
+
if invalid:
|
|
117
|
+
for json_path, error in invalid:
|
|
118
|
+
tc_print(f" - {json_path} ({error})")
|
|
119
|
+
else:
|
|
120
|
+
tc_print(" (none)")
|
|
121
|
+
tc_line()
|
|
122
|
+
|
|
123
|
+
errors: dict = {}
|
|
124
|
+
if invalid:
|
|
125
|
+
errors["admin_users"] = {"invalid_json_paths": invalid}
|
|
126
|
+
return errors
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def audit_admin_users(
|
|
130
|
+
ctx: typer.Context,
|
|
131
|
+
installation_path: types.installation_path_type,
|
|
132
|
+
): # pragma NO COVER command
|
|
133
|
+
"""List configured admin users and flag any with invalid JSONPath.
|
|
134
|
+
|
|
135
|
+
Each stored 'AdminUser.json_path' is re-validated against the
|
|
136
|
+
currently-loaded JSONPath environment. An entry whose query no
|
|
137
|
+
longer compiles (e.g. because the meta-config filter function it
|
|
138
|
+
referenced has been removed) is reported as an audit error.
|
|
139
|
+
"""
|
|
140
|
+
quiet = ctx.obj["quiet"]
|
|
141
|
+
errors = _audit_admin_users_section(ctx, installation_path)
|
|
142
|
+
audit_common._emit_errors(errors, quiet)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from soliplex import installation
|
|
6
|
+
from soliplex import models
|
|
7
|
+
from soliplex.cli import types
|
|
8
|
+
from soliplex.cli.audit import _common as audit_common
|
|
9
|
+
from soliplex.cli.audit import interpolation as audit_interpolation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _invalid_completions(
|
|
13
|
+
the_installation: installation.Installation,
|
|
14
|
+
) -> dict:
|
|
15
|
+
errors = {}
|
|
16
|
+
|
|
17
|
+
# Deliberately bypass auth check done by 'get_room_configs' here.
|
|
18
|
+
available_completions = the_installation._config.completion_configs
|
|
19
|
+
|
|
20
|
+
for compl_config in available_completions.values():
|
|
21
|
+
try:
|
|
22
|
+
models.Completion.from_config(compl_config)
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
errors.setdefault("completions", {})[compl_config.id] = str(exc)
|
|
25
|
+
|
|
26
|
+
return errors
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _audit_completions_section(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
installation_path: types.installation_path_type,
|
|
32
|
+
) -> dict: # pragma NO COVER UI ONLY
|
|
33
|
+
"""Print the completions section (rule header + per-completion entry)."""
|
|
34
|
+
quiet = ctx.obj["quiet"]
|
|
35
|
+
the_installation = audit_common._get_installation(ctx, installation_path)
|
|
36
|
+
tc_line, tc_rule, tc_print, _ = audit_common._quiet_console_funcs(quiet)
|
|
37
|
+
|
|
38
|
+
tc_line()
|
|
39
|
+
tc_rule("Configured completions")
|
|
40
|
+
tc_line()
|
|
41
|
+
|
|
42
|
+
errors = _invalid_completions(the_installation)
|
|
43
|
+
invalid_completions = errors.get("completions", {})
|
|
44
|
+
|
|
45
|
+
available_completions = the_installation._config.completion_configs
|
|
46
|
+
for compl_config in available_completions.values():
|
|
47
|
+
tc_print(f"- [ {compl_config.id} ] {compl_config.name}: ")
|
|
48
|
+
exc = invalid_completions.get(compl_config.id)
|
|
49
|
+
if exc is not None:
|
|
50
|
+
tc_print(f" ERROR: {exc}")
|
|
51
|
+
else:
|
|
52
|
+
tc_print(" OK")
|
|
53
|
+
tc_line()
|
|
54
|
+
|
|
55
|
+
interpolation_errors = (
|
|
56
|
+
audit_interpolation._invalid_completion_interpolations(
|
|
57
|
+
the_installation
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
audit_interpolation._print_interpolation_findings(
|
|
61
|
+
tc_print, interpolation_errors
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return errors | interpolation_errors
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def audit_completions(
|
|
68
|
+
ctx: typer.Context,
|
|
69
|
+
installation_path: types.installation_path_type,
|
|
70
|
+
): # pragma NO COVER command
|
|
71
|
+
"""List completions defined in the installation"""
|
|
72
|
+
quiet = ctx.obj["quiet"]
|
|
73
|
+
errors = _audit_completions_section(ctx, installation_path)
|
|
74
|
+
audit_common._emit_errors(errors, quiet)
|