adapt-server 0.2.3__tar.gz → 0.2.5__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.
- {adapt_server-0.2.3 → adapt_server-0.2.5}/PKG-INFO +78 -2
- {adapt_server-0.2.3 → adapt_server-0.2.5}/README.md +75 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/__init__.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/ui.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/app.py +119 -32
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/cache.py +19 -11
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/cli.py +27 -2
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/addsuperuser.py +19 -4
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/__init__.py +3 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/create_user.py +17 -3
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/check.py +16 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/list_endpoints.py +1 -1
- adapt_server-0.2.5/adapt/commands/passwords.py +116 -0
- adapt_server-0.2.5/adapt/commands/reindex.py +44 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/config.py +23 -3
- adapt_server-0.2.5/adapt/mcp.py +180 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/permissions.py +26 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/__init__.py +3 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/base.py +55 -3
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/csv_plugin.py +9 -12
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/dataset_plugin.py +39 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/excel_plugin.py +6 -8
- adapt_server-0.2.5/adapt/plugins/file_plugin.py +108 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/html_plugin.py +52 -3
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/markdown_plugin.py +50 -3
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/media_plugin.py +20 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/parquet_plugin.py +11 -14
- adapt_server-0.2.5/adapt/routes.py +127 -0
- adapt_server-0.2.5/adapt/routes_search.py +173 -0
- adapt_server-0.2.5/adapt/search.py +399 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/admin_base.html +7 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/base.html +10 -0
- adapt_server-0.2.5/adapt/templates/search_results.html +57 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/utils/__init__.py +5 -5
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/utils/query.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/PKG-INFO +78 -2
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/SOURCES.txt +11 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/requires.txt +2 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/pyproject.toml +3 -2
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_admin.py +144 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_cache.py +7 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_cli_integration.py +20 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_config.py +2 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_csv_plugin.py +63 -1
- adapt_server-0.2.5/tests/test_file_plugin.py +109 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_html_plugin.py +8 -2
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_integration.py +84 -1
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_markdown_plugin.py +3 -1
- adapt_server-0.2.5/tests/test_mcp.py +355 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_parquet_plugin.py +49 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_plugin_interface.py +2 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_readonly.py +6 -8
- adapt_server-0.2.5/tests/test_routes.py +28 -0
- adapt_server-0.2.5/tests/test_search.py +548 -0
- adapt_server-0.2.3/adapt/routes.py +0 -69
- {adapt_server-0.2.3 → adapt_server-0.2.5}/LICENSE +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/__main__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/api_keys.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/audit_logs.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/cache.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/groups.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/models.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/permissions.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/resources.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/admin/users.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/api_keys.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/audit.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/auth/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/auth/dependencies.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/auth/password.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/auth/routes.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/auth/session.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/add_to_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/create_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/create_permissions.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/delete_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/delete_user.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/list_groups.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/list_resources.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/list_users.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/admin/remove_from_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/commands/serve.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/discovery.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/models.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/plugins/python_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/security.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/security_urls.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/static/admin/app.js +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/static/admin/style.css +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/storage.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/admin/index.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/datatable.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/landing.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/login.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/media_gallery.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/media_player.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt/templates/profile.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/dependency_links.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/entry_points.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/adapt_server.egg-info/top_level.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/setup.cfg +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_auth.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_constraints.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_dataset_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_dependencies.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_discovery.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_on_delete.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_phase3.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_python_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.5}/tests/test_security_urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adapt-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Adaptive file-backed FastAPI server that turns datasets into CRUD APIs and UIs.
|
|
5
5
|
Author-email: notesofcliff <notesofcliff@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: fastapi
|
|
24
|
+
Requires-Dist: fastapi>=0.115
|
|
25
25
|
Requires-Dist: uvicorn
|
|
26
26
|
Requires-Dist: sqlmodel
|
|
27
27
|
Requires-Dist: pendulum
|
|
@@ -36,6 +36,7 @@ Requires-Dist: pillow
|
|
|
36
36
|
Requires-Dist: python-json-logger
|
|
37
37
|
Requires-Dist: pandas
|
|
38
38
|
Requires-Dist: fastparquet
|
|
39
|
+
Requires-Dist: mcp<2,>=1.28
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: pytest; extra == "dev"
|
|
41
42
|
Requires-Dist: httpx; extra == "dev"
|
|
@@ -51,6 +52,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
|
51
52
|
- Markdown/HTML become browsable pages
|
|
52
53
|
- Media files become streaming endpoints and player/gallery UIs
|
|
53
54
|
- Python files can register custom routers
|
|
55
|
+
- Everything is searchable in one place via full-text `/search`
|
|
56
|
+
- Everything is reachable by agentic tools via an MCP server at `/mcp`
|
|
54
57
|
|
|
55
58
|
## Quick Start
|
|
56
59
|
|
|
@@ -81,6 +84,8 @@ Useful URLs:
|
|
|
81
84
|
- `/api/<resource>` resource API
|
|
82
85
|
- `/ui/<resource>` resource UI
|
|
83
86
|
- `/schema/<resource>` resource schema
|
|
87
|
+
- `/search` full-text search across every resource you can read
|
|
88
|
+
- `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
|
|
84
89
|
|
|
85
90
|
## What Adapt Generates
|
|
86
91
|
|
|
@@ -135,6 +140,75 @@ This reflects the current implementation in the codebase.
|
|
|
135
140
|
- Caching with invalidation on mutations
|
|
136
141
|
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
137
142
|
- Plugin architecture with companion overrides in `.adapt/`
|
|
143
|
+
- Permission-filtered full-text search across every resource type
|
|
144
|
+
- MCP server for agentic tool access, mounted alongside the REST API
|
|
145
|
+
|
|
146
|
+
## Full-Text Search
|
|
147
|
+
|
|
148
|
+
`GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
|
|
149
|
+
in one ranked list, filtered to what the caller may read — a query term that
|
|
150
|
+
matches a resource you can't see never shows up, and never leaks via the
|
|
151
|
+
result count either.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The index refreshes incrementally on startup (`search_on_startup`, default
|
|
158
|
+
`true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
|
|
159
|
+
[API Reference](docs/manual/api_reference.md#search-endpoint) for query
|
|
160
|
+
parameters and result shape.
|
|
161
|
+
|
|
162
|
+
## MCP Interface
|
|
163
|
+
|
|
164
|
+
Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
165
|
+
server at `/mcp`, on the same host/port as everything else, exposing five
|
|
166
|
+
tools that wrap the same permission checks and plugin methods as the REST
|
|
167
|
+
API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
|
|
168
|
+
`search`. There's no separate process, no separate API surface, and no
|
|
169
|
+
extra permission model to maintain.
|
|
170
|
+
|
|
171
|
+
Minimal walkthrough — create an account for the agent, grant it read access,
|
|
172
|
+
mint an API key, and connect a client:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
adapt addsuperuser /path/to/docroot --username admin
|
|
176
|
+
adapt serve /path/to/docroot &
|
|
177
|
+
|
|
178
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
179
|
+
adapt admin create-user /path/to/docroot --username agent --password <strong-password>
|
|
180
|
+
adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Log in as `agent` and self-issue an API key from `/profile` (any
|
|
184
|
+
authenticated user can create their own key — no superuser needed), then
|
|
185
|
+
point a client at `/mcp` with that key:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Claude Code CLI
|
|
189
|
+
claude mcp add --transport http adapt http://localhost:8000/mcp \
|
|
190
|
+
--header "X-API-Key: <key>"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
// Generic MCP client config (Claude Desktop and similar)
|
|
195
|
+
{
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"adapt": {
|
|
198
|
+
"url": "http://localhost:8000/mcp",
|
|
199
|
+
"headers": { "X-API-Key": "<key>" }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
MCP requires an API key on every call — there's no session-cookie or
|
|
206
|
+
anonymous path, since MCP has no concept of a browser session. Set
|
|
207
|
+
`mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
|
|
208
|
+
remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
|
|
209
|
+
reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
|
|
210
|
+
For dataset reads, `sort` is the column name and `order` must be `asc` or
|
|
211
|
+
`desc`.
|
|
138
212
|
|
|
139
213
|
## Dataset Mutation Envelope
|
|
140
214
|
|
|
@@ -156,6 +230,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
|
|
|
156
230
|
adapt check <root>
|
|
157
231
|
adapt addsuperuser <root> --username <name>
|
|
158
232
|
adapt list-endpoints <root>
|
|
233
|
+
adapt reindex <root> [--force]
|
|
159
234
|
adapt admin list-resources <root>
|
|
160
235
|
adapt admin create-permissions <root> __all__
|
|
161
236
|
```
|
|
@@ -169,6 +244,7 @@ Detailed docs live under `docs/manual/`.
|
|
|
169
244
|
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
170
245
|
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
171
246
|
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
247
|
+
- MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
|
|
172
248
|
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
173
249
|
|
|
174
250
|
## License
|
|
@@ -6,6 +6,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
|
6
6
|
- Markdown/HTML become browsable pages
|
|
7
7
|
- Media files become streaming endpoints and player/gallery UIs
|
|
8
8
|
- Python files can register custom routers
|
|
9
|
+
- Everything is searchable in one place via full-text `/search`
|
|
10
|
+
- Everything is reachable by agentic tools via an MCP server at `/mcp`
|
|
9
11
|
|
|
10
12
|
## Quick Start
|
|
11
13
|
|
|
@@ -36,6 +38,8 @@ Useful URLs:
|
|
|
36
38
|
- `/api/<resource>` resource API
|
|
37
39
|
- `/ui/<resource>` resource UI
|
|
38
40
|
- `/schema/<resource>` resource schema
|
|
41
|
+
- `/search` full-text search across every resource you can read
|
|
42
|
+
- `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
|
|
39
43
|
|
|
40
44
|
## What Adapt Generates
|
|
41
45
|
|
|
@@ -90,6 +94,75 @@ This reflects the current implementation in the codebase.
|
|
|
90
94
|
- Caching with invalidation on mutations
|
|
91
95
|
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
92
96
|
- Plugin architecture with companion overrides in `.adapt/`
|
|
97
|
+
- Permission-filtered full-text search across every resource type
|
|
98
|
+
- MCP server for agentic tool access, mounted alongside the REST API
|
|
99
|
+
|
|
100
|
+
## Full-Text Search
|
|
101
|
+
|
|
102
|
+
`GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
|
|
103
|
+
in one ranked list, filtered to what the caller may read — a query term that
|
|
104
|
+
matches a resource you can't see never shows up, and never leaks via the
|
|
105
|
+
result count either.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The index refreshes incrementally on startup (`search_on_startup`, default
|
|
112
|
+
`true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
|
|
113
|
+
[API Reference](docs/manual/api_reference.md#search-endpoint) for query
|
|
114
|
+
parameters and result shape.
|
|
115
|
+
|
|
116
|
+
## MCP Interface
|
|
117
|
+
|
|
118
|
+
Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
119
|
+
server at `/mcp`, on the same host/port as everything else, exposing five
|
|
120
|
+
tools that wrap the same permission checks and plugin methods as the REST
|
|
121
|
+
API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
|
|
122
|
+
`search`. There's no separate process, no separate API surface, and no
|
|
123
|
+
extra permission model to maintain.
|
|
124
|
+
|
|
125
|
+
Minimal walkthrough — create an account for the agent, grant it read access,
|
|
126
|
+
mint an API key, and connect a client:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
adapt addsuperuser /path/to/docroot --username admin
|
|
130
|
+
adapt serve /path/to/docroot &
|
|
131
|
+
|
|
132
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
133
|
+
adapt admin create-user /path/to/docroot --username agent --password <strong-password>
|
|
134
|
+
adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Log in as `agent` and self-issue an API key from `/profile` (any
|
|
138
|
+
authenticated user can create their own key — no superuser needed), then
|
|
139
|
+
point a client at `/mcp` with that key:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Claude Code CLI
|
|
143
|
+
claude mcp add --transport http adapt http://localhost:8000/mcp \
|
|
144
|
+
--header "X-API-Key: <key>"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
// Generic MCP client config (Claude Desktop and similar)
|
|
149
|
+
{
|
|
150
|
+
"mcpServers": {
|
|
151
|
+
"adapt": {
|
|
152
|
+
"url": "http://localhost:8000/mcp",
|
|
153
|
+
"headers": { "X-API-Key": "<key>" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
MCP requires an API key on every call — there's no session-cookie or
|
|
160
|
+
anonymous path, since MCP has no concept of a browser session. Set
|
|
161
|
+
`mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
|
|
162
|
+
remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
|
|
163
|
+
reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
|
|
164
|
+
For dataset reads, `sort` is the column name and `order` must be `asc` or
|
|
165
|
+
`desc`.
|
|
93
166
|
|
|
94
167
|
## Dataset Mutation Envelope
|
|
95
168
|
|
|
@@ -111,6 +184,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
|
|
|
111
184
|
adapt check <root>
|
|
112
185
|
adapt addsuperuser <root> --username <name>
|
|
113
186
|
adapt list-endpoints <root>
|
|
187
|
+
adapt reindex <root> [--force]
|
|
114
188
|
adapt admin list-resources <root>
|
|
115
189
|
adapt admin create-permissions <root> __all__
|
|
116
190
|
```
|
|
@@ -124,6 +198,7 @@ Detailed docs live under `docs/manual/`.
|
|
|
124
198
|
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
125
199
|
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
126
200
|
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
201
|
+
- MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
|
|
127
202
|
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
128
203
|
|
|
129
204
|
## License
|
|
@@ -30,4 +30,4 @@ def admin_ui(request: Request):
|
|
|
30
30
|
|
|
31
31
|
# Serve the admin template
|
|
32
32
|
logger.debug("Rendering admin UI for superuser %s", user.username)
|
|
33
|
-
return request.app.state.templates.TemplateResponse("admin/index.html", context)
|
|
33
|
+
return request.app.state.templates.TemplateResponse(request, "admin/index.html", context)
|
|
@@ -12,7 +12,6 @@ from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redi
|
|
|
12
12
|
from fastapi.openapi.utils import get_openapi
|
|
13
13
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
14
14
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
15
|
-
from fastapi.routing import APIRoute
|
|
16
15
|
from fastapi.staticfiles import StaticFiles
|
|
17
16
|
from fastapi.templating import Jinja2Templates
|
|
18
17
|
from sqlmodel import Session, select, delete
|
|
@@ -25,11 +24,13 @@ from .admin import router as admin_router
|
|
|
25
24
|
from .config import AdaptConfig
|
|
26
25
|
from .discovery import discover_resources
|
|
27
26
|
from .permissions import PermissionChecker
|
|
28
|
-
from .routes import generate_routes
|
|
27
|
+
from .routes import build_resource_registry, generate_routes, resource_namespaces as _routes_resource_namespaces
|
|
28
|
+
from .routes_search import router as search_router, safe_snippet
|
|
29
29
|
from .storage import User, DBSession, init_database
|
|
30
30
|
from .locks import LockManager
|
|
31
|
+
from .mcp import build_mcp_server
|
|
31
32
|
from .utils import build_accessible_ui_links
|
|
32
|
-
from . import cache
|
|
33
|
+
from . import cache, search
|
|
33
34
|
from .security import (
|
|
34
35
|
apply_security_headers,
|
|
35
36
|
build_allowed_hosts,
|
|
@@ -45,7 +46,7 @@ logger = logging.getLogger(__name__)
|
|
|
45
46
|
|
|
46
47
|
_DOCS_INTERNAL_PATHS = frozenset({"/docs", "/docs/", "/docs/oauth2-redirect", "/openapi.json"})
|
|
47
48
|
_PUBLIC_OPENAPI_PATHS = frozenset({"/", "/auth/login", "/health"})
|
|
48
|
-
_AUTHENTICATED_OPENAPI_PATHS = frozenset({"/auth/logout", "/auth/me", "/profile", "/api/apikeys"})
|
|
49
|
+
_AUTHENTICATED_OPENAPI_PATHS = frozenset({"/auth/logout", "/auth/me", "/profile", "/api/apikeys", "/search"})
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
async def cleanup_expired_sessions(engine, interval_hours=24):
|
|
@@ -72,13 +73,7 @@ def _normalize_path(path: str) -> str:
|
|
|
72
73
|
|
|
73
74
|
def _resource_namespaces(resource) -> set[str]:
|
|
74
75
|
"""Return the supported permission namespaces for a discovered resource."""
|
|
75
|
-
|
|
76
|
-
namespace_with_ext = resource.relative_path.as_posix()
|
|
77
|
-
if "sub_namespace" in resource.metadata:
|
|
78
|
-
suffix = f"/{resource.metadata['sub_namespace']}"
|
|
79
|
-
namespace_no_ext += suffix
|
|
80
|
-
namespace_with_ext += suffix
|
|
81
|
-
return {namespace_no_ext, namespace_with_ext}
|
|
76
|
+
return set(_routes_resource_namespaces(resource))
|
|
82
77
|
|
|
83
78
|
|
|
84
79
|
def _all_resource_namespaces(resources) -> set[str]:
|
|
@@ -125,14 +120,14 @@ def _extract_resource_namespace(path: str, all_namespaces: set[str]) -> str | No
|
|
|
125
120
|
return None
|
|
126
121
|
|
|
127
122
|
|
|
128
|
-
def
|
|
129
|
-
"""Decide whether
|
|
130
|
-
path = _normalize_path(
|
|
123
|
+
def _operation_is_visible(path: str, operation: dict, request: Request, user: User | None, all_namespaces: set[str], visible_namespaces: set[str]) -> bool:
|
|
124
|
+
"""Decide whether an OpenAPI operation should appear in the current request's schema."""
|
|
125
|
+
path = _normalize_path(path)
|
|
131
126
|
|
|
132
127
|
if path in _DOCS_INTERNAL_PATHS:
|
|
133
128
|
return False
|
|
134
129
|
|
|
135
|
-
if path.startswith("/admin") or "admin" in (
|
|
130
|
+
if path.startswith("/admin") or "admin" in (operation.get("tags") or []):
|
|
136
131
|
return bool(user and getattr(user, "is_superuser", False))
|
|
137
132
|
|
|
138
133
|
if path in _PUBLIC_OPENAPI_PATHS:
|
|
@@ -160,24 +155,73 @@ def _route_is_visible(route: APIRoute, request: Request, user: User | None, all_
|
|
|
160
155
|
return False
|
|
161
156
|
|
|
162
157
|
|
|
158
|
+
def _iter_schema_refs(node):
|
|
159
|
+
"""Yield component schema names referenced anywhere within a JSON-like node."""
|
|
160
|
+
if isinstance(node, dict):
|
|
161
|
+
ref = node.get("$ref")
|
|
162
|
+
if isinstance(ref, str) and ref.startswith("#/components/schemas/"):
|
|
163
|
+
yield ref.removeprefix("#/components/schemas/")
|
|
164
|
+
for value in node.values():
|
|
165
|
+
yield from _iter_schema_refs(value)
|
|
166
|
+
elif isinstance(node, list):
|
|
167
|
+
for item in node:
|
|
168
|
+
yield from _iter_schema_refs(item)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _prune_unreferenced_components(schema: dict) -> None:
|
|
172
|
+
"""Drop component schemas no longer reachable from any kept path.
|
|
173
|
+
|
|
174
|
+
Filtering removes paths but leaves their request/response models behind in
|
|
175
|
+
components.schemas — a non-superuser would otherwise receive the
|
|
176
|
+
field-level shape of admin-only models like UserPublic or Permission.
|
|
177
|
+
"""
|
|
178
|
+
components = schema.get("components") or {}
|
|
179
|
+
schemas = components.get("schemas")
|
|
180
|
+
if not schemas:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
reachable: set[str] = set()
|
|
184
|
+
pending = set(_iter_schema_refs(schema.get("paths", {})))
|
|
185
|
+
while pending:
|
|
186
|
+
name = pending.pop()
|
|
187
|
+
if name in reachable:
|
|
188
|
+
continue
|
|
189
|
+
reachable.add(name)
|
|
190
|
+
pending.update(_iter_schema_refs(schemas.get(name, {})))
|
|
191
|
+
|
|
192
|
+
pruned = {name: definition for name, definition in schemas.items() if name in reachable}
|
|
193
|
+
if pruned:
|
|
194
|
+
components["schemas"] = pruned
|
|
195
|
+
else:
|
|
196
|
+
components.pop("schemas", None)
|
|
197
|
+
if not components:
|
|
198
|
+
schema.pop("components", None)
|
|
199
|
+
|
|
200
|
+
|
|
163
201
|
def _build_openapi_schema(app: FastAPI, request: Request, user: User | None) -> dict:
|
|
164
202
|
"""Build a filtered OpenAPI schema for the current request context."""
|
|
165
|
-
|
|
166
|
-
visible_namespaces = _visible_resource_namespaces(request, user)
|
|
167
|
-
visible_routes = [
|
|
168
|
-
route
|
|
169
|
-
for route in app.routes
|
|
170
|
-
if isinstance(route, APIRoute)
|
|
171
|
-
and route.include_in_schema
|
|
172
|
-
and _route_is_visible(route, request, user, all_namespaces, visible_namespaces)
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
return get_openapi(
|
|
203
|
+
schema = get_openapi(
|
|
176
204
|
title=app.title,
|
|
177
205
|
version=app.version,
|
|
178
206
|
description=app.description,
|
|
179
|
-
routes=
|
|
207
|
+
routes=app.routes,
|
|
180
208
|
)
|
|
209
|
+
all_namespaces = _all_resource_namespaces(request.app.state.resources)
|
|
210
|
+
visible_namespaces = _visible_resource_namespaces(request, user)
|
|
211
|
+
|
|
212
|
+
visible_paths: dict[str, dict] = {}
|
|
213
|
+
for path, operations in schema.get("paths", {}).items():
|
|
214
|
+
kept = {
|
|
215
|
+
method: operation
|
|
216
|
+
for method, operation in operations.items()
|
|
217
|
+
if _operation_is_visible(path, operation, request, user, all_namespaces, visible_namespaces)
|
|
218
|
+
}
|
|
219
|
+
if kept:
|
|
220
|
+
visible_paths[path] = kept
|
|
221
|
+
|
|
222
|
+
schema["paths"] = visible_paths
|
|
223
|
+
_prune_unreferenced_components(schema)
|
|
224
|
+
return schema
|
|
181
225
|
|
|
182
226
|
|
|
183
227
|
def _visible_resource_paths(request: Request, user: User | None) -> list[str]:
|
|
@@ -192,14 +236,25 @@ def _visible_resource_paths(request: Request, user: User | None) -> list[str]:
|
|
|
192
236
|
|
|
193
237
|
@asynccontextmanager
|
|
194
238
|
async def lifespan(app: FastAPI):
|
|
195
|
-
"""Handle application startup and shutdown events.
|
|
239
|
+
"""Handle application startup and shutdown events.
|
|
240
|
+
|
|
241
|
+
A mounted MCP sub-app's own `lifespan` is never invoked by the ASGI
|
|
242
|
+
server: Starlette's `Mount` only forwards `http`/`websocket` scope types,
|
|
243
|
+
never `lifespan`. So the MCP session manager's `run()` context must be
|
|
244
|
+
entered here, alongside the rest of this app's own startup/shutdown.
|
|
245
|
+
"""
|
|
196
246
|
# Startup: Start background cleanup task
|
|
197
247
|
engine = app.state.db_engine
|
|
198
248
|
cleanup_task = asyncio.create_task(cleanup_expired_sessions(engine))
|
|
199
249
|
logger.debug("Application startup: background cleanup task started")
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
|
|
251
|
+
mcp_server = getattr(app.state, "mcp_server", None)
|
|
252
|
+
if mcp_server is not None:
|
|
253
|
+
async with mcp_server.session_manager.run():
|
|
254
|
+
yield
|
|
255
|
+
else:
|
|
256
|
+
yield
|
|
257
|
+
|
|
203
258
|
# Shutdown: Could add cleanup logic here if needed
|
|
204
259
|
cleanup_task.cancel()
|
|
205
260
|
try:
|
|
@@ -219,12 +274,16 @@ def _init_infrastructure(config: AdaptConfig):
|
|
|
219
274
|
"""
|
|
220
275
|
engine = init_database(config.db_path)
|
|
221
276
|
cache.configure(str(config.db_path))
|
|
277
|
+
search.configure(str(config.db_path))
|
|
222
278
|
lock_manager = LockManager(engine)
|
|
223
279
|
cleaned = lock_manager.release_stale_locks(max_age_seconds=300)
|
|
224
280
|
if cleaned > 0:
|
|
225
281
|
logging.warning("Cleaned %d stale locks on startup", cleaned)
|
|
226
282
|
resources = discover_resources(config.root, config)
|
|
227
283
|
logger.debug("Discovered %d resources", len(resources))
|
|
284
|
+
if config.search_on_startup:
|
|
285
|
+
# Incremental: unchanged files are skipped, so restarts stay cheap.
|
|
286
|
+
search.reindex_all(resources, config)
|
|
228
287
|
return engine, lock_manager, resources
|
|
229
288
|
|
|
230
289
|
|
|
@@ -246,6 +305,7 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
246
305
|
app.state.use_tls = bool(config.tls_cert and config.tls_key)
|
|
247
306
|
app.state.lock_manager = lock_manager
|
|
248
307
|
app.state.resources = resources
|
|
308
|
+
app.state.resource_registry = build_resource_registry(resources, config)
|
|
249
309
|
|
|
250
310
|
allowed_hosts = build_allowed_hosts(config.host)
|
|
251
311
|
if allowed_hosts != ["*"]:
|
|
@@ -254,6 +314,9 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
254
314
|
# Set up Jinja2 templates
|
|
255
315
|
templates_dir = Path(__file__).parent / "templates"
|
|
256
316
|
templates = Jinja2Templates(directory=str(templates_dir))
|
|
317
|
+
# Search snippets hold raw docroot text; this escapes them and restores only
|
|
318
|
+
# the <mark> highlights. See routes_search.safe_snippet.
|
|
319
|
+
templates.env.filters["safe_snippet"] = safe_snippet
|
|
257
320
|
app.state.templates = templates
|
|
258
321
|
|
|
259
322
|
# Mount static files
|
|
@@ -310,8 +373,32 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
310
373
|
# Mount admin routes
|
|
311
374
|
app.include_router(admin_router)
|
|
312
375
|
|
|
376
|
+
# Mount search routes
|
|
377
|
+
app.include_router(search_router)
|
|
378
|
+
|
|
313
379
|
# Generate and mount routes
|
|
314
|
-
generate_routes(app,
|
|
380
|
+
generate_routes(app, app.state.resource_registry)
|
|
381
|
+
|
|
382
|
+
# Mount the MCP server, exposing resources as agent-facing tools
|
|
383
|
+
if config.mcp_enabled:
|
|
384
|
+
mcp_server = build_mcp_server(config)
|
|
385
|
+
mcp_app = mcp_server.streamable_http_app()
|
|
386
|
+
# A mounted sub-app's `request.app` is itself, not the main app (see
|
|
387
|
+
# lifespan() docstring for the related lifespan gotcha) — mirror the
|
|
388
|
+
# slice of state every tool/helper needs onto it.
|
|
389
|
+
mcp_app.state.db_engine = engine
|
|
390
|
+
mcp_app.state.resources = resources
|
|
391
|
+
mcp_app.state.config = config
|
|
392
|
+
mcp_app.state.lock_manager = lock_manager
|
|
393
|
+
mcp_app.state.resource_registry = app.state.resource_registry
|
|
394
|
+
# The outer app's own middleware still runs for /mcp requests (it wraps
|
|
395
|
+
# routing, including the Mount), and by the time it inspects
|
|
396
|
+
# `request.app.state` post-routing, `scope["app"]` has already been
|
|
397
|
+
# overwritten to `mcp_app` — so anything that middleware reads off
|
|
398
|
+
# app.state must be mirrored here too, not just what the tools need.
|
|
399
|
+
mcp_app.state.use_tls = app.state.use_tls
|
|
400
|
+
app.state.mcp_server = mcp_server
|
|
401
|
+
app.mount("/mcp", mcp_app)
|
|
315
402
|
|
|
316
403
|
@app.get("/openapi.json", include_in_schema=False)
|
|
317
404
|
def openapi_schema(request: Request):
|
|
@@ -8,12 +8,22 @@ import threading
|
|
|
8
8
|
import logging
|
|
9
9
|
from datetime import datetime, timedelta, timezone
|
|
10
10
|
import pickle
|
|
11
|
+
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
CACHE_TABLE = "cache"
|
|
15
|
-
_lock = threading.
|
|
16
|
+
_lock = threading.RLock()
|
|
16
17
|
_db_path: str | None = None
|
|
18
|
+
_CREATE_CACHE_TABLE_SQL = f"""
|
|
19
|
+
CREATE TABLE IF NOT EXISTS {CACHE_TABLE} (
|
|
20
|
+
key TEXT PRIMARY KEY,
|
|
21
|
+
value BLOB,
|
|
22
|
+
expires_at DATETIME,
|
|
23
|
+
resource TEXT,
|
|
24
|
+
user TEXT
|
|
25
|
+
)
|
|
26
|
+
"""
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
def _default_db_path() -> str:
|
|
@@ -35,24 +45,22 @@ def configure(db_path: str | None) -> None:
|
|
|
35
45
|
def _get_conn():
|
|
36
46
|
"""Get a SQLite connection to the cache database."""
|
|
37
47
|
path = _db_path or _default_db_path()
|
|
48
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
38
49
|
conn = sqlite3.connect(path, check_same_thread=False)
|
|
39
50
|
conn.row_factory = sqlite3.Row
|
|
51
|
+
_ensure_cache_table(conn)
|
|
40
52
|
return conn
|
|
41
53
|
|
|
54
|
+
|
|
55
|
+
def _ensure_cache_table(conn: sqlite3.Connection) -> None:
|
|
56
|
+
"""Ensure the cache table exists for the active database connection."""
|
|
57
|
+
conn.execute(_CREATE_CACHE_TABLE_SQL)
|
|
58
|
+
conn.commit()
|
|
59
|
+
|
|
42
60
|
def init_cache_table():
|
|
43
61
|
"""Initialize the cache table in the SQLite database."""
|
|
44
62
|
with _lock:
|
|
45
63
|
conn = _get_conn()
|
|
46
|
-
conn.execute(f"""
|
|
47
|
-
CREATE TABLE IF NOT EXISTS {CACHE_TABLE} (
|
|
48
|
-
key TEXT PRIMARY KEY,
|
|
49
|
-
value BLOB,
|
|
50
|
-
expires_at DATETIME,
|
|
51
|
-
resource TEXT,
|
|
52
|
-
user TEXT
|
|
53
|
-
)
|
|
54
|
-
""")
|
|
55
|
-
conn.commit()
|
|
56
64
|
conn.close()
|
|
57
65
|
logger.debug("Cache table initialized")
|
|
58
66
|
|
|
@@ -7,7 +7,7 @@ import logging.config
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from .config import AdaptConfig
|
|
10
|
-
from .commands import check, addsuperuser, list_endpoints, serve
|
|
10
|
+
from .commands import check, addsuperuser, list_endpoints, reindex, serve
|
|
11
11
|
from .commands.admin import run_admin
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
@@ -36,10 +36,20 @@ def main() -> None:
|
|
|
36
36
|
user_parser.add_argument("root", nargs="?", default=".", help="Document root containing the SQLite store")
|
|
37
37
|
user_parser.add_argument("--username", required=True, help="Username for the superuser")
|
|
38
38
|
user_parser.add_argument("--password", help="Password (will prompt if missing)")
|
|
39
|
+
user_parser.add_argument("--password-confirm", help="Password confirmation for non-interactive use")
|
|
40
|
+
user_parser.add_argument(
|
|
41
|
+
"--allow-weak-password",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Allow a weak password without the safety confirmation prompt",
|
|
44
|
+
)
|
|
39
45
|
|
|
40
46
|
list_parser = subparsers.add_parser("list-endpoints", help="List the auto-generated REST/UI endpoints")
|
|
41
47
|
list_parser.add_argument("root", nargs="?", default=".", help="Document root to inspect")
|
|
42
48
|
|
|
49
|
+
reindex_parser = subparsers.add_parser("reindex", help="Rebuild the full-text search index")
|
|
50
|
+
reindex_parser.add_argument("root", nargs="?", default=".", help="Document root to index")
|
|
51
|
+
reindex_parser.add_argument("--force", action="store_true", help="Reindex even unchanged files")
|
|
52
|
+
|
|
43
53
|
admin_parser = subparsers.add_parser("admin", help="Admin tasks")
|
|
44
54
|
admin_subparsers = admin_parser.add_subparsers(dest="admin_command", required=True)
|
|
45
55
|
admin_list_resources_parser = admin_subparsers.add_parser("list-resources", help="List discovered resources")
|
|
@@ -61,6 +71,12 @@ def main() -> None:
|
|
|
61
71
|
admin_create_user_parser.add_argument("root", nargs="?", default=".", help="Document root")
|
|
62
72
|
admin_create_user_parser.add_argument("--username", required=True, help="Username")
|
|
63
73
|
admin_create_user_parser.add_argument("--password", help="Password (will prompt if missing)")
|
|
74
|
+
admin_create_user_parser.add_argument("--password-confirm", help="Password confirmation for non-interactive use")
|
|
75
|
+
admin_create_user_parser.add_argument(
|
|
76
|
+
"--allow-weak-password",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Allow a weak password without the safety confirmation prompt",
|
|
79
|
+
)
|
|
64
80
|
admin_create_user_parser.add_argument("--superuser", action="store_true", help="Create as superuser")
|
|
65
81
|
|
|
66
82
|
admin_delete_user_parser = admin_subparsers.add_parser("delete-user", help="Delete user")
|
|
@@ -112,10 +128,19 @@ def main() -> None:
|
|
|
112
128
|
check.run_check(Path(args.root).resolve())
|
|
113
129
|
elif args.command == "addsuperuser":
|
|
114
130
|
logger.info("Running addsuperuser command for username=%s", args.username)
|
|
115
|
-
addsuperuser.run_add_superuser(
|
|
131
|
+
addsuperuser.run_add_superuser(
|
|
132
|
+
Path(args.root).resolve(),
|
|
133
|
+
args.username,
|
|
134
|
+
args.password,
|
|
135
|
+
password_confirm=args.password_confirm,
|
|
136
|
+
allow_weak_password=args.allow_weak_password,
|
|
137
|
+
)
|
|
116
138
|
elif args.command == "list-endpoints":
|
|
117
139
|
logger.info("Running list-endpoints command with root=%s", args.root)
|
|
118
140
|
list_endpoints.run_list_endpoints(Path(args.root).resolve())
|
|
141
|
+
elif args.command == "reindex":
|
|
142
|
+
logger.info("Running reindex command with root=%s force=%s", args.root, args.force)
|
|
143
|
+
reindex.run_reindex(Path(args.root).resolve(), force=args.force)
|
|
119
144
|
elif args.command == "admin":
|
|
120
145
|
logger.info("Running admin command: %s", args.admin_command)
|
|
121
146
|
run_admin(args)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import getpass
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
import logging
|
|
4
3
|
|
|
@@ -7,17 +6,26 @@ from sqlmodel import Session, select
|
|
|
7
6
|
from ..config import AdaptConfig
|
|
8
7
|
from ..storage import User, init_database
|
|
9
8
|
from ..auth.password import hash_password
|
|
9
|
+
from .passwords import resolve_password
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def run_add_superuser(
|
|
14
|
+
def run_add_superuser(
|
|
15
|
+
root: Path,
|
|
16
|
+
username: str,
|
|
17
|
+
password: str | None,
|
|
18
|
+
password_confirm: str | None = None,
|
|
19
|
+
allow_weak_password: bool = False,
|
|
20
|
+
) -> None:
|
|
15
21
|
"""Create a new superuser account.
|
|
16
22
|
|
|
17
23
|
Args:
|
|
18
24
|
root: The root directory path for the Adapt configuration.
|
|
19
25
|
username: The username for the new superuser.
|
|
20
26
|
password: The password for the new superuser. If None, prompts for input.
|
|
27
|
+
password_confirm: Confirmation for non-interactive password entry.
|
|
28
|
+
allow_weak_password: Allow weak passwords without an interactive override prompt.
|
|
21
29
|
|
|
22
30
|
Returns:
|
|
23
31
|
None
|
|
@@ -27,8 +35,15 @@ def run_add_superuser(root: Path, username: str, password: str | None) -> None:
|
|
|
27
35
|
"""
|
|
28
36
|
config = AdaptConfig(root=root)
|
|
29
37
|
engine = init_database(config.db_path)
|
|
38
|
+
password = resolve_password(
|
|
39
|
+
username=username,
|
|
40
|
+
password=password,
|
|
41
|
+
password_confirm=password_confirm,
|
|
42
|
+
allow_weak_password=allow_weak_password,
|
|
43
|
+
)
|
|
30
44
|
if password is None:
|
|
31
|
-
|
|
45
|
+
logger.warning("Aborted superuser creation for %s due to password validation", username)
|
|
46
|
+
return
|
|
32
47
|
|
|
33
48
|
hashed = hash_password(password)
|
|
34
49
|
logger.debug("Hashed password for user %s", username)
|
|
@@ -43,4 +58,4 @@ def run_add_superuser(root: Path, username: str, password: str | None) -> None:
|
|
|
43
58
|
session.add(user)
|
|
44
59
|
session.commit()
|
|
45
60
|
logger.info("Created superuser '%s'", username)
|
|
46
|
-
print(f"Created superuser '{username}'")
|
|
61
|
+
print(f"Created superuser '{username}'")
|