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.
Files changed (122) hide show
  1. {soliplex-0.82/src/soliplex.egg-info → soliplex-0.83}/PKG-INFO +6 -2
  2. {soliplex-0.82 → soliplex-0.83}/pyproject.toml +21 -6
  3. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agents.py +2 -8
  4. soliplex-0.83/src/soliplex/cli/audit/__init__.py +128 -0
  5. soliplex-0.83/src/soliplex/cli/audit/_common.py +89 -0
  6. soliplex-0.83/src/soliplex/cli/audit/admin_users.py +142 -0
  7. soliplex-0.83/src/soliplex/cli/audit/completions.py +74 -0
  8. soliplex-0.83/src/soliplex/cli/audit/databases.py +329 -0
  9. soliplex-0.83/src/soliplex/cli/audit/environment.py +77 -0
  10. soliplex-0.83/src/soliplex/cli/audit/installation.py +111 -0
  11. soliplex-0.83/src/soliplex/cli/audit/interpolation.py +525 -0
  12. soliplex-0.83/src/soliplex/cli/audit/logfire.py +45 -0
  13. soliplex-0.83/src/soliplex/cli/audit/logging.py +76 -0
  14. soliplex-0.83/src/soliplex/cli/audit/oidc.py +67 -0
  15. soliplex-0.83/src/soliplex/cli/audit/ollama.py +178 -0
  16. soliplex-0.83/src/soliplex/cli/audit/quizzes.py +80 -0
  17. soliplex-0.83/src/soliplex/cli/audit/room_authz.py +197 -0
  18. soliplex-0.83/src/soliplex/cli/audit/rooms.py +242 -0
  19. soliplex-0.83/src/soliplex/cli/audit/secrets.py +53 -0
  20. soliplex-0.83/src/soliplex/cli/audit/skills.py +134 -0
  21. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/cli_util.py +26 -3
  22. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/database.py +1 -16
  23. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/main.py +30 -19
  24. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/agents.py +2 -2
  25. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/rooms.py +4 -1
  26. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/skills.py +12 -42
  27. {soliplex-0.82 → soliplex-0.83}/src/soliplex/installation.py +29 -9
  28. {soliplex-0.82 → soliplex-0.83}/src/soliplex/loggers.py +3 -0
  29. {soliplex-0.82 → soliplex-0.83/src/soliplex.egg-info}/PKG-INFO +6 -2
  30. {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/SOURCES.txt +17 -1
  31. {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/requires.txt +7 -1
  32. soliplex-0.82/src/soliplex/cli/audit.py +0 -2234
  33. {soliplex-0.82 → soliplex-0.83}/LICENSE +0 -0
  34. {soliplex-0.82 → soliplex-0.83}/MANIFEST.in +0 -0
  35. {soliplex-0.82 → soliplex-0.83}/README.md +0 -0
  36. {soliplex-0.82 → soliplex-0.83}/example/skills/bare-bones/SKILL.md +0 -0
  37. {soliplex-0.82 → soliplex-0.83}/example/skills/stanzas/SKILL.md +0 -0
  38. {soliplex-0.82 → soliplex-0.83}/setup.cfg +0 -0
  39. {soliplex-0.82 → soliplex-0.83}/skills/soliplex-docs/SKILL.md +0 -0
  40. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/__init__.py +0 -0
  41. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/parser.py +0 -0
  42. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/persistence.py +0 -0
  43. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/schema.py +0 -0
  44. {soliplex-0.82 → soliplex-0.83}/src/soliplex/agui/util.py +0 -0
  45. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/__init__.py +0 -0
  46. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/env.py +0 -0
  47. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/216d48e1e2e5_soliplex_v0_53.py +0 -0
  48. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/4b63e5f3f39e_soliplex_v0_51_1.py +0 -0
  49. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/63edaa5987f6_soliplex_v0_67.py +0 -0
  50. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/a1c7d3e90b42_soliplex_v0_80.py +0 -0
  51. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/b7e2f41c9d05_soliplex_v0_82.py +0 -0
  52. {soliplex-0.82 → soliplex-0.83}/src/soliplex/alembic_migrations/versions/d5009d4f9874_soliplex_v0_44.py +0 -0
  53. {soliplex-0.82 → soliplex-0.83}/src/soliplex/authn.py +0 -0
  54. {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/__init__.py +0 -0
  55. {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/persistence.py +0 -0
  56. {soliplex-0.82 → soliplex-0.83}/src/soliplex/authz/schema.py +0 -0
  57. {soliplex-0.82 → soliplex-0.83}/src/soliplex/capabilities/filesystem.py +0 -0
  58. {soliplex-0.82 → soliplex-0.83}/src/soliplex/capabilities/rag_audit.py +0 -0
  59. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/admin_users.py +0 -0
  60. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/ask.py +0 -0
  61. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/ollama.py +0 -0
  62. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/room_authz.py +0 -0
  63. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/serve.py +0 -0
  64. {soliplex-0.82 → soliplex-0.83}/src/soliplex/cli/types.py +0 -0
  65. {soliplex-0.82 → soliplex-0.83}/src/soliplex/completions.py +0 -0
  66. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/__init__.py +0 -0
  67. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/_utils.py +0 -0
  68. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/agui.py +0 -0
  69. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/authsystem.py +0 -0
  70. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/completions.py +0 -0
  71. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/exceptions.py +0 -0
  72. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/installation.py +0 -0
  73. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/interpolation.py +0 -0
  74. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/logfire.py +0 -0
  75. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/meta.py +0 -0
  76. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/middleware.py +0 -0
  77. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/quizzes.py +0 -0
  78. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/rag.py +0 -0
  79. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/routing.py +0 -0
  80. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/secrets.py +0 -0
  81. {soliplex-0.82 → soliplex-0.83}/src/soliplex/config/tools.py +0 -0
  82. {soliplex-0.82 → soliplex-0.83}/src/soliplex/examples.py +0 -0
  83. {soliplex-0.82 → soliplex-0.83}/src/soliplex/log_ingest.py +0 -0
  84. {soliplex-0.82 → soliplex-0.83}/src/soliplex/main.py +0 -0
  85. {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_auth.py +0 -0
  86. {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_client.py +0 -0
  87. {soliplex-0.82 → soliplex-0.83}/src/soliplex/mcp_server.py +0 -0
  88. {soliplex-0.82 → soliplex-0.83}/src/soliplex/models.py +0 -0
  89. {soliplex-0.82 → soliplex-0.83}/src/soliplex/ollama.py +0 -0
  90. {soliplex-0.82 → soliplex-0.83}/src/soliplex/quizzes.py +0 -0
  91. {soliplex-0.82 → soliplex-0.83}/src/soliplex/rag_audit.py +0 -0
  92. {soliplex-0.82 → soliplex-0.83}/src/soliplex/sandbox_audit.py +0 -0
  93. {soliplex-0.82 → soliplex-0.83}/src/soliplex/secrets.py +0 -0
  94. {soliplex-0.82 → soliplex-0.83}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  95. {soliplex-0.82 → soliplex-0.83}/src/soliplex/skills/bwrap_sandbox/__init__.py +0 -0
  96. {soliplex-0.82 → soliplex-0.83}/src/soliplex/titles.py +0 -0
  97. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/__init__.py +0 -0
  98. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/agui_run_feedback.py +0 -0
  99. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/file_uploads.py +0 -0
  100. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tools/rag.py +0 -0
  101. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/cli.py +0 -0
  102. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/main.py +0 -0
  103. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/rest_api.py +0 -0
  104. {soliplex-0.82 → soliplex-0.83}/src/soliplex/tui/serve.py +0 -0
  105. {soliplex-0.82 → soliplex-0.83}/src/soliplex/util.py +0 -0
  106. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/__init__.py +0 -0
  107. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/agui.py +0 -0
  108. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/authn.py +0 -0
  109. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/authz.py +0 -0
  110. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/completions.py +0 -0
  111. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/installation.py +0 -0
  112. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/log_ingest.py +0 -0
  113. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/quizzes.py +0 -0
  114. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/room_file_uploads.py +0 -0
  115. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/rooms.py +0 -0
  116. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/sandbox_workdirs.py +0 -0
  117. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/stats.py +0 -0
  118. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/streaming.py +0 -0
  119. {soliplex-0.82 → soliplex-0.83}/src/soliplex/views/thread_file_uploads.py +0 -0
  120. {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/dependency_links.txt +0 -0
  121. {soliplex-0.82 → soliplex-0.83}/src/soliplex.egg-info/entry_points.txt +0 -0
  122. {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.82
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.85,>=0.84.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.82"
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.84.0, < 0.85",
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/analysis capabilities attach picture chunks to search results as
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, HR_VisionCapabilities):
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)