harborrag-app 2.0.0a1__py3-none-any.whl

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 (123) hide show
  1. harborrag_app/__init__.py +14 -0
  2. harborrag_app/api/README.md +470 -0
  3. harborrag_app/api/__init__.py +0 -0
  4. harborrag_app/api/app.py +193 -0
  5. harborrag_app/api/auth/__init__.py +1 -0
  6. harborrag_app/api/auth/base.py +20 -0
  7. harborrag_app/api/auth/dependencies.py +143 -0
  8. harborrag_app/api/auth/hmac.py +74 -0
  9. harborrag_app/api/auth/principal.py +37 -0
  10. harborrag_app/api/capacity.py +237 -0
  11. harborrag_app/api/capacity_dependency.py +47 -0
  12. harborrag_app/api/dependencies.py +21 -0
  13. harborrag_app/api/errors.py +179 -0
  14. harborrag_app/api/export_openapi.py +40 -0
  15. harborrag_app/api/metrics.py +113 -0
  16. harborrag_app/api/middleware.py +127 -0
  17. harborrag_app/api/router.py +32 -0
  18. harborrag_app/api/routes/__init__.py +34 -0
  19. harborrag_app/api/routes/activity.py +50 -0
  20. harborrag_app/api/routes/health.py +29 -0
  21. harborrag_app/api/routes/legacy_diagnostics.py +59 -0
  22. harborrag_app/api/routes/legacy_ingestions.py +186 -0
  23. harborrag_app/api/routes/metrics.py +60 -0
  24. harborrag_app/api/routes/projects.py +79 -0
  25. harborrag_app/api/routes/settings.py +24 -0
  26. harborrag_app/api/routes/sources.py +148 -0
  27. harborrag_app/api/schemas.py +60 -0
  28. harborrag_app/api/settings.py +80 -0
  29. harborrag_app/api/v1/__init__.py +1 -0
  30. harborrag_app/api/v1/admin/__init__.py +5 -0
  31. harborrag_app/api/v1/admin/dependencies.py +29 -0
  32. harborrag_app/api/v1/admin/routes.py +113 -0
  33. harborrag_app/api/v1/admin/schemas.py +28 -0
  34. harborrag_app/api/v1/agent/__init__.py +5 -0
  35. harborrag_app/api/v1/agent/dependencies.py +62 -0
  36. harborrag_app/api/v1/agent/routes.py +180 -0
  37. harborrag_app/api/v1/agent/schemas.py +73 -0
  38. harborrag_app/api/v1/chat/__init__.py +5 -0
  39. harborrag_app/api/v1/chat/dependencies.py +53 -0
  40. harborrag_app/api/v1/chat/routes.py +147 -0
  41. harborrag_app/api/v1/chat/schemas.py +70 -0
  42. harborrag_app/api/v1/connections/__init__.py +5 -0
  43. harborrag_app/api/v1/connections/dependencies.py +23 -0
  44. harborrag_app/api/v1/connections/routes.py +42 -0
  45. harborrag_app/api/v1/connections/schemas.py +27 -0
  46. harborrag_app/api/v1/ingestion/__init__.py +5 -0
  47. harborrag_app/api/v1/ingestion/commands.py +18 -0
  48. harborrag_app/api/v1/ingestion/dependencies.py +62 -0
  49. harborrag_app/api/v1/ingestion/routes.py +262 -0
  50. harborrag_app/api/v1/ingestion/schemas.py +196 -0
  51. harborrag_app/api/v1/retrieval/__init__.py +5 -0
  52. harborrag_app/api/v1/retrieval/dependencies.py +64 -0
  53. harborrag_app/api/v1/retrieval/routes.py +161 -0
  54. harborrag_app/api/v1/retrieval/schemas.py +218 -0
  55. harborrag_app/cli/README.md +385 -0
  56. harborrag_app/cli/__init__.py +0 -0
  57. harborrag_app/cli/banner.py +25 -0
  58. harborrag_app/cli/commands/__init__.py +0 -0
  59. harborrag_app/cli/commands/chat.py +75 -0
  60. harborrag_app/cli/commands/doctor.py +37 -0
  61. harborrag_app/cli/commands/ingest.py +309 -0
  62. harborrag_app/cli/commands/retrieve.py +117 -0
  63. harborrag_app/cli/dashboard/README.md +17 -0
  64. harborrag_app/cli/dashboard/__init__.py +4 -0
  65. harborrag_app/cli/dashboard/dialogs.py +42 -0
  66. harborrag_app/cli/dashboard/rendering.py +127 -0
  67. harborrag_app/cli/dashboard/schemas.py +82 -0
  68. harborrag_app/cli/dashboard/screen.py +215 -0
  69. harborrag_app/cli/dashboard/styles.py +143 -0
  70. harborrag_app/cli/environment.py +58 -0
  71. harborrag_app/cli/main.py +99 -0
  72. harborrag_app/cli/rendering.py +326 -0
  73. harborrag_app/cli/rendering_values.py +80 -0
  74. harborrag_app/cli/runner.py +252 -0
  75. harborrag_app/cli/stages.py +172 -0
  76. harborrag_app/workflow_control/README.md +25 -0
  77. harborrag_app/workflow_control/__init__.py +31 -0
  78. harborrag_app/workflow_control/agent/__init__.py +7 -0
  79. harborrag_app/workflow_control/agent/client.py +87 -0
  80. harborrag_app/workflow_control/agent/options.py +15 -0
  81. harborrag_app/workflow_control/agent/service.py +274 -0
  82. harborrag_app/workflow_control/chat/__init__.py +7 -0
  83. harborrag_app/workflow_control/chat/client.py +74 -0
  84. harborrag_app/workflow_control/chat/options.py +17 -0
  85. harborrag_app/workflow_control/chat/presenters.py +61 -0
  86. harborrag_app/workflow_control/chat/service.py +264 -0
  87. harborrag_app/workflow_control/composition/__init__.py +1 -0
  88. harborrag_app/workflow_control/composition/factories.py +60 -0
  89. harborrag_app/workflow_control/composition/resources.py +105 -0
  90. harborrag_app/workflow_control/composition/selection.py +25 -0
  91. harborrag_app/workflow_control/composition/service.py +303 -0
  92. harborrag_app/workflow_control/control_plane/__init__.py +1 -0
  93. harborrag_app/workflow_control/control_plane/effect_recovery.py +160 -0
  94. harborrag_app/workflow_control/control_plane/metrics.py +44 -0
  95. harborrag_app/workflow_control/control_plane/reads.py +72 -0
  96. harborrag_app/workflow_control/control_plane/writes.py +260 -0
  97. harborrag_app/workflow_control/errors.py +109 -0
  98. harborrag_app/workflow_control/ingestion/__init__.py +1 -0
  99. harborrag_app/workflow_control/ingestion/client.py +91 -0
  100. harborrag_app/workflow_control/ingestion/connections.py +44 -0
  101. harborrag_app/workflow_control/ingestion/models.py +25 -0
  102. harborrag_app/workflow_control/ingestion/ports.py +114 -0
  103. harborrag_app/workflow_control/ingestion/presenters.py +264 -0
  104. harborrag_app/workflow_control/ingestion/progress_bridge.py +151 -0
  105. harborrag_app/workflow_control/ingestion/recovery.py +114 -0
  106. harborrag_app/workflow_control/ingestion/service.py +344 -0
  107. harborrag_app/workflow_control/ingestion/task_pages.py +91 -0
  108. harborrag_app/workflow_control/ingestion/temporal.py +194 -0
  109. harborrag_app/workflow_control/memory/__init__.py +6 -0
  110. harborrag_app/workflow_control/memory/composition.py +56 -0
  111. harborrag_app/workflow_control/memory/sessions.py +59 -0
  112. harborrag_app/workflow_control/ports.py +269 -0
  113. harborrag_app/workflow_control/retrieval/__init__.py +1 -0
  114. harborrag_app/workflow_control/retrieval/client.py +85 -0
  115. harborrag_app/workflow_control/retrieval/graph.py +125 -0
  116. harborrag_app/workflow_control/retrieval/presentation.py +49 -0
  117. harborrag_app/workflow_control/retrieval/query.py +63 -0
  118. harborrag_app/workflow_control/schemas.py +13 -0
  119. harborrag_app-2.0.0a1.dist-info/METADATA +134 -0
  120. harborrag_app-2.0.0a1.dist-info/RECORD +123 -0
  121. harborrag_app-2.0.0a1.dist-info/WHEEL +4 -0
  122. harborrag_app-2.0.0a1.dist-info/entry_points.txt +2 -0
  123. harborrag_app-2.0.0a1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,14 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from harborrag_app.workflow_control import AppResponse, BaseAppService
4
+
5
+ try:
6
+ __version__ = version("harborrag-app")
7
+ except PackageNotFoundError: # pragma: no cover - source checkout without installation
8
+ __version__ = "0+unknown"
9
+
10
+ __all__ = [
11
+ "AppResponse",
12
+ "BaseAppService",
13
+ "__version__",
14
+ ]
@@ -0,0 +1,470 @@
1
+ # Control Plane API
2
+
3
+ The `harborrag_app.api` package is HarborRAG's FastAPI boundary. It validates
4
+ HTTP input, authenticates callers, delegates workflow operations to the
5
+ transport-neutral application service, and maps results onto stable HTTP
6
+ responses. Connector execution, parsing, chunking, indexing, and Temporal
7
+ workflow rules remain outside this package.
8
+
9
+ Operational health and documentation routes remain under `/api/v1`. Stable
10
+ public ingestion, retrieval, chat, and agent resources are mounted under `/v1`.
11
+
12
+ For browser convenience, `/` and `/docs` redirect to `/api/v1/docs` while
13
+ documentation is enabled. When documentation is disabled, `/docs` remains
14
+ closed and `/` redirects to `/api/v1/health`.
15
+
16
+ ## Install and run
17
+
18
+ Install the API dependencies from a source checkout:
19
+
20
+ ```bash
21
+ uv sync --package harborrag-app --extra api
22
+ ```
23
+
24
+ Start the application factory directly:
25
+
26
+ ```bash
27
+ uv run uvicorn harborrag_app.api.app:create_fastapi_app \
28
+ --factory \
29
+ --host 127.0.0.1 \
30
+ --port 8000
31
+ ```
32
+
33
+ For the supported local Docker topology, use:
34
+
35
+ ```bash
36
+ scripts/deployment/dev.sh up
37
+ curl --fail http://127.0.0.1:8000/api/v1/health
38
+ curl --fail -H "Authorization: Bearer $ADMIN_TOKEN" \
39
+ http://127.0.0.1:8000/api/v1/metrics
40
+ ```
41
+
42
+ The development helper starts the data services, Temporal, an ingestion
43
+ worker, and the API, then waits for its process health check. If those dependencies are
44
+ already running, `scripts/deployment/dev.sh api` starts only the API. API process
45
+ and authentication values belong in the ignored `env/.env.api`; embedding
46
+ credentials come from `env/.env.models` for synchronous vector retrieval.
47
+ Connector credentials remain isolated in the worker. See the repository
48
+ [deployment guide](../../../../../docs/developers/deployment/README.md) for
49
+ environment preparation and shutdown.
50
+
51
+ The local API container listens on `0.0.0.0` inside Docker but Compose publishes
52
+ port 8000 only on host loopback. Accordingly, the development environment file
53
+ sets `HARBORRAG_ALLOW_INSECURE_DEV=true` alongside `HARBORRAG_AUTH_MODE=none`.
54
+ Do not publish that configuration on a non-loopback host address; use HMAC auth
55
+ before exposing the API outside the trusted local development network.
56
+
57
+ ## Composition modes
58
+
59
+ The application lifespan constructs one `AppService` and closes it on process
60
+ shutdown.
61
+
62
+ | Condition | Composition |
63
+ | --- | --- |
64
+ | `HARBORRAG_ENV=dev` without `HARBORRAG_CONTROL_DB_URL` | Development control-database stub plus the real Temporal client |
65
+ | A control-database URL is set | Migrated runtime control-plane repositories plus the real Temporal client |
66
+ | `HARBORRAG_ENV=prod` | Requires an explicit control-database URL and authenticated API mode |
67
+
68
+ Development mode changes only control-plane repository composition. Ingestion
69
+ requests still go to the configured Temporal service.
70
+
71
+ ## HTTP routes
72
+
73
+ | Method | Path | Minimum role | Behavior |
74
+ | --- | --- | --- | --- |
75
+ The API serves two prefixes, and the distinction matters:
76
+
77
+ - **`/v1/...` is the public contract** - ingestion, retrieval, chat, agent, admin. This is
78
+ what applications integrate against.
79
+ - **`/api/v1/...` is the operational surface** - health, readiness, metrics, and the
80
+ console-facing project/source/activity/settings routes. Treat it as deployment
81
+ plumbing, not an application API.
82
+
83
+ `/api/v1/docs` and `/api/v1/openapi.json` are served when `HARBORRAG_DOCS_ENABLED` is set;
84
+ `GET /` and `GET /docs` redirect there (or to `/api/v1/health` when docs are disabled).
85
+ The live schema is always authoritative - prefer it to this table.
86
+
87
+ ### Public contract (`/v1`)
88
+
89
+ | Method | Path | Minimum role | Behavior |
90
+ | --- | --- | --- | --- |
91
+ | `POST` | `/v1/ingestions` | `editor` | Submit a durable ingestion task |
92
+ | `GET` | `/v1/ingestions` | `reader` | List tasks newest first, cursor-paginated |
93
+ | `GET` | `/v1/ingestions/{task_id}` | `reader` | Read Postgres-authoritative task progress |
94
+ | `GET` | `/v1/ingestions/{task_id}/documents` | `reader` | Read cursor-paginated document outcomes |
95
+ | `GET` | `/v1/ingestions/{task_id}/stream` | `reader` | Stream task progress as Server-Sent Events |
96
+ | `POST` | `/v1/ingestions/{task_id}/cancel` | `editor` | Request graceful cancellation |
97
+ | `POST` | `/v1/ingestions/{task_id}/retry-failures` | `editor` | Retry selected or all retryable failures |
98
+ | `GET` | `/v1/connections` | `reader` | List enabled connections submittable by `connection_id` |
99
+ | `POST` | `/v1/chat/sessions` | `reader` | Create a persisted chat session and greeting (`201`) |
100
+ | `POST` | `/v1/chat/completions` | `reader` | Retrieval-grounded JSON or SSE chat completion |
101
+ | `POST` | `/v1/agent/sessions` | `reader` | Create a persisted agent session and greeting (`201`) |
102
+ | `POST` | `/v1/agent/completions` | `reader` | Bounded multi-turn model/tool completion |
103
+ | `POST` | `/v1/agent/runs/{run_id}/resume` | `reader` | Continue a run that stopped before finishing |
104
+ | `POST` | `/v1/retrieval/vector` | `reader` | Dense, sparse, or hybrid vector search |
105
+ | `POST` | `/v1/retrieval/graph/triplets` | `reader` | Match subject-predicate-object records |
106
+ | `POST` | `/v1/retrieval/graph/paths` | `reader` | Find bounded paths between graph nodes |
107
+ | `POST` | `/v1/retrieval/graph/subgraphs` | `reader` | Expand a bounded graph neighborhood |
108
+ | `GET` | `/v1/admin/projections/{tenant}` | `admin` | Inspect projection state for a tenant |
109
+ | `DELETE` | `/v1/admin/projections/{tenant}` | `admin` | Drop a tenant's rebuildable projections |
110
+
111
+ ### Operational surface (`/api/v1`)
112
+
113
+ | Method | Path | Minimum role | Behavior |
114
+ | --- | --- | --- | --- |
115
+ | `GET` | `/api/v1/health` | Public | Process liveness; does not probe dependencies |
116
+ | `GET` | `/api/v1/readyz` | Public | Dependency-aware readiness; `503` when runtime composition is unavailable |
117
+ | `GET` | `/api/v1/metrics` | `admin` | Prometheus API, Python runtime, and process metrics |
118
+ | `GET` | `/api/v1/metrics/ingestion` | `reader` | Ingestion-specific metrics |
119
+ | `GET` | `/api/v1/projects` | `reader` | List projects |
120
+ | `GET` | `/api/v1/projects/{project_id}` | `reader` | Read one project |
121
+ | `GET` | `/api/v1/sources` | `reader` | List configured sources |
122
+ | `POST` | `/api/v1/sources` | `editor` | Create a source |
123
+ | `GET` | `/api/v1/sources/{source_id}` | `reader` | Read one source |
124
+ | `PATCH` | `/api/v1/sources/{source_id}` | `editor` | Update a source |
125
+ | `DELETE` | `/api/v1/sources/{source_id}` | `editor` | Delete a source |
126
+ | `GET` | `/api/v1/activity` | `reader` | Recent activity feed |
127
+ | `GET` | `/api/v1/settings` | `reader` | Effective non-secret settings |
128
+
129
+ ### Deprecated
130
+
131
+ These predate the `/v1` contract, still appear in the generated schema, and will be
132
+ removed. Use the `/v1/ingestions*` routes instead.
133
+
134
+ | Method | Path | Minimum role |
135
+ | --- | --- | --- |
136
+ | `GET` | `/api/v1/diagnostics` | `admin` |
137
+ | `POST` | `/api/v1/ingestions` | `editor` |
138
+ | `GET` | `/api/v1/ingestions/{run_id}` | `reader` |
139
+ | `GET` | `/api/v1/ingestions/{run_id}/result` | `reader` |
140
+ | `POST` | `/api/v1/ingestions/{run_id}/actions` | `editor` |
141
+
142
+ The API authenticates the principal and accepts tenant as one explicit,
143
+ top-level request field. It defaults to `DEFAULT`; callers cannot bypass tenant
144
+ isolation through retrieval filters.
145
+
146
+ ### Generate a chat completion
147
+
148
+ Chat uses the `chat` section of `config/models.yaml`, resolves credentials from
149
+ `env/.env.models`, and retrieves tenant-scoped evidence before calling the
150
+ model. Callers may choose per-request graph observation and streaming, but
151
+ cannot submit a system prompt, provider
152
+ credentials, model overrides, custom tools, or adapter-specific parameters.
153
+
154
+ ```bash
155
+ curl --fail-with-body \
156
+ --request POST \
157
+ --header 'Content-Type: application/json' \
158
+ --data '{"tenant":"DEFAULT"}' \
159
+ http://127.0.0.1:8000/v1/chat/sessions
160
+
161
+ curl --fail-with-body --request POST \
162
+ --header 'Content-Type: application/json' \
163
+ --data '{"session_id":"session-...","prompt":"What is HarborRAG?"}' \
164
+ http://127.0.0.1:8000/v1/chat/completions
165
+ ```
166
+
167
+ The session response contains a generated `session_id` and greeting. The completion POST
168
+ requires that session ID and a prompt in its JSON body. `stream=true` changes the response to
169
+ SSE. The two latest PostgreSQL-backed turns are recalled under the tenant,
170
+ authenticated principal, and session identity. Requests are marked sensitive
171
+ so model-response caching remains disabled. Completion endpoints accept POST
172
+ only, keeping prompts and other sensitive content out of request URLs and access logs.
173
+
174
+ ### Run a multi-turn agent
175
+
176
+ The agent uses the same model and memory identity but can execute multiple
177
+ model/tool hops before synthesizing an answer. Only runtime-owned read tools
178
+ are exposed. `graph_search` controls whether graph triplet, path, and subgraph
179
+ tools are available, and `max_steps` bounds model/tool rounds from 1 to 8.
180
+
181
+ ```bash
182
+ curl --fail-with-body \
183
+ --request POST \
184
+ --header 'Content-Type: application/json' \
185
+ --data '{"tenant":"DEFAULT"}' \
186
+ http://127.0.0.1:8000/v1/agent/sessions
187
+
188
+ curl --fail-with-body --request POST \
189
+ --header 'Content-Type: application/json' \
190
+ --data '{"session_id":"session-...","prompt":"Connect this release policy to its owner.","graph_search":true,"max_steps":4}' \
191
+ http://127.0.0.1:8000/v1/agent/completions
192
+ ```
193
+
194
+ The POST returns a `session_id` and greeting. Agent completions return
195
+ aggregate token usage, turn count, and a safe tool trace containing tool names
196
+ and success status. Raw tool arguments and results are not returned.
197
+
198
+ The former `POST /v1/retrieval/search` route has been removed. Retrieval
199
+ operations are intentionally not exposed as GET: query text and nested filters
200
+ belong in a validated request body and should not be copied into URLs, access
201
+ logs, browser history, or intermediary cache keys. A future GET endpoint should
202
+ represent a persisted retrieval resource; searches are not persisted today.
203
+
204
+ ### Retrieve evidence
205
+
206
+ Vector search has one contract for both simple and advanced callers. Omitting
207
+ controls uses hybrid retrieval; advanced callers may select `dense`, `sparse`,
208
+ or `hybrid`, apply metadata filters, observe graph context, set a score
209
+ threshold, and control content or metadata projection.
210
+
211
+ `filters` is optional. Omit it for an unfiltered search; when supplied, use
212
+ real indexed metadata keys such as `{"category": "architecture"}`. The
213
+ `additionalProp1` names shown by generic JSON-object schema renderers are
214
+ placeholders, not fields recognized by HarborRAG.
215
+
216
+ This is a synchronous `POST`: a successful response contains at most `top_k`
217
+ ranked results immediately. It never returns an ingestion-style `task_id` and
218
+ does not require a follow-up GET. The optional `request_id` in the response is
219
+ only a trace/correlation identifier. A retrieval GET should be added only if
220
+ search results become persisted resources.
221
+
222
+ ```bash
223
+ curl --fail-with-body \
224
+ --request POST \
225
+ --header 'Content-Type: application/json' \
226
+ --data '{
227
+ "query": "publication policy",
228
+ "tenant": "DEFAULT",
229
+ "top_k": 10,
230
+ "lane": "hybrid",
231
+ "filters": {"category": "architecture"},
232
+ "observe_graph": true,
233
+ "score_threshold": 0.2,
234
+ "include_content": true,
235
+ "include_metadata": true
236
+ }' \
237
+ http://127.0.0.1:8000/v1/retrieval/vector
238
+ ```
239
+
240
+ Graph operations use portable graph records rather than FalkorDB query syntax.
241
+ Triplets accept any combination containing at least one of `subject`,
242
+ `predicate`, or `object`; paths require distinct start and end nodes; subgraph
243
+ expansion requires one start node. Path depth is capped at 8 and all graph
244
+ result counts are capped at 100.
245
+
246
+ ### Submit ingestion
247
+
248
+ ```bash
249
+ curl --fail-with-body \
250
+ --request POST \
251
+ --header 'Content-Type: application/json' \
252
+ --header 'Idempotency-Key: local-release-notes-2026-08' \
253
+ --data '{
254
+ "connection_id": "harborrag-workspace",
255
+ "tenant": "DEFAULT",
256
+ "mode": "incremental"
257
+ }' \
258
+ http://127.0.0.1:8000/v1/ingestions
259
+ ```
260
+
261
+ The application generates a public UUIDv4 task ID. Before Temporal is
262
+ called, it persists the source scope and a `PENDING` task in Postgres. Reusing
263
+ an idempotency key with the same public request returns the existing task;
264
+ reusing it for a different request returns a conflict.
265
+
266
+ Task paths intentionally continue to accept opaque strings, so tasks created
267
+ by older versions with `ing_...` or `retry_...` identifiers remain queryable.
268
+
269
+ The connection must be enabled in the same connector configuration used by the
270
+ worker. Provider, scope, paths, attachment/comment policy, limits, and
271
+ credentials come from `config/connectors.yaml`; the API does not duplicate or
272
+ override them. The API resolves only connector identity and processing
273
+ fingerprints; connector calls and model execution remain worker activities.
274
+
275
+ `mode` defaults to `incremental`. It discovers the configured scope and skips
276
+ unchanged documents. `force` fetches and evaluates unchanged documents again,
277
+ but it does not delete data, force a new deterministic document version, or
278
+ rebuild an already-active projection merely because its storage collection was
279
+ renamed. See [Ingestion modes](../../../../../docs/users/ingestion-modes.md) for
280
+ the complete behavior and reindex guidance.
281
+
282
+ ### List connections and tasks
283
+
284
+ ```bash
285
+ curl --fail-with-body http://127.0.0.1:8000/v1/connections
286
+
287
+ curl --fail-with-body 'http://127.0.0.1:8000/v1/ingestions?status=RUNNING&limit=25'
288
+ ```
289
+
290
+ `/v1/connections` returns the `connection_id` and `source_type` of every
291
+ connection enabled in `config/connectors.yaml`, which is the only place those
292
+ names are defined. Nothing else about a connector is exposed: settings,
293
+ environment references, and credential references stay server-side. A
294
+ definition the catalog could not parse is omitted rather than failing the
295
+ listing; submitting it still reports its own configuration error.
296
+
297
+ `/v1/ingestions` lists tasks newest submission first and pages with an opaque
298
+ `cursor`, exactly like `/v1/ingestions/{task_id}/documents`. Each item carries
299
+ the same fields as the single-task response. `tenant` narrows the listing to
300
+ one tenant the caller may read; omitting it reads every tenant in the caller's
301
+ own scope, so a tenant-scoped token never sees another tenant's tasks and an
302
+ out-of-scope `tenant` is rejected rather than answered with an empty page.
303
+
304
+ ### Inspect and control a task
305
+
306
+ ```bash
307
+ curl --fail-with-body \
308
+ http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a
309
+
310
+ curl --fail-with-body \
311
+ 'http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a/documents?limit=50'
312
+
313
+ curl --fail-with-body \
314
+ --request POST \
315
+ http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a/cancel
316
+ ```
317
+
318
+ Cancellation is asynchronous and takes effect at safe workflow boundaries.
319
+ Published document versions remain active. The retry endpoint creates a new
320
+ UUIDv4 task and starts at the earliest reusable durable artifact; it does
321
+ not refetch a source when a raw or later artifact exists.
322
+
323
+ ## Authentication and roles
324
+
325
+ `HARBORRAG_AUTH_MODE` supports:
326
+
327
+ - `none`: local development only; assigns an implicit `owner` principal;
328
+ - `hmac`: verifies HS256 bearer JWTs using `HARBORRAG_AUTH_SECRET`;
329
+ - `oidc`: reserved but not implemented; application creation fails fast.
330
+
331
+ Production refuses `auth_mode=none`. HMAC secrets must contain at least 32
332
+ UTF-8 bytes. Tokens must contain `sub`, `role`, `tenants`, `iat`, `exp`, `iss`,
333
+ and `aud` claims. `tenants` is a non-empty list of tenant identifiers the
334
+ principal may access; a global role does not grant access to other tenants.
335
+ The default issuer is `harborrag`, the default audience is `harborrag-api`, and
336
+ the role order is:
337
+
338
+ ```text
339
+ reader < editor < admin < owner
340
+ ```
341
+
342
+ Send authenticated requests with:
343
+
344
+ ```http
345
+ Authorization: Bearer JWT
346
+ ```
347
+
348
+ The liveness endpoint intentionally remains unauthenticated. Prometheus metrics
349
+ require an admin bearer token at `/api/v1/metrics`; configure a protected scrape
350
+ credential or a separate operational listener when the API is
351
+ not running on a trusted service network.
352
+
353
+ ## Configuration
354
+
355
+ API process settings use the `HARBORRAG_` prefix.
356
+
357
+ | Variable | Default | Purpose |
358
+ | --- | --- | --- |
359
+ | `HARBORRAG_HOST` | `127.0.0.1` | Uvicorn bind host; containers override this to `0.0.0.0`, where disabled auth requires an explicit insecure-development opt-in |
360
+ | `HARBORRAG_PORT` | `8000` | Uvicorn bind port used by the container command |
361
+ | `HARBORRAG_ENV` | `dev` | `dev` or `prod` process policy |
362
+ | `HARBORRAG_AUTH_MODE` | `none` | `none`, `hmac`, or `oidc` |
363
+ | `HARBORRAG_ALLOW_INSECURE_DEV` | `false` | Explicitly acknowledge disabled auth on a non-loopback development listener |
364
+ | `HARBORRAG_AUTH_SECRET` | unset | Shared HS256 secret |
365
+ | `HARBORRAG_AUTH_ISSUER` | `harborrag` | Required JWT issuer |
366
+ | `HARBORRAG_AUTH_AUDIENCE` | `harborrag-api` | Required JWT audience |
367
+ | `HARBORRAG_CORS_ORIGINS` | `[]` | JSON list of allowed browser origins |
368
+ | `HARBORRAG_DOCS_ENABLED` | `true` in dev | Enables Swagger and the live OpenAPI route |
369
+ | `HARBORRAG_MAX_REQUEST_BODY_BYTES` | `1048576` | Maximum request body accepted before JSON parsing |
370
+ | `HARBORRAG_API_CAPACITY_REDIS_URL` | unset in dev; required in prod | Redis backend for cross-replica principal limits |
371
+ | `HARBORRAG_API_CAPACITY_ALLOW_INSECURE_REMOTE` | `false` | Development-only acknowledgement for remote plaintext Redis |
372
+ | `HARBORRAG_API_REQUESTS_PER_MINUTE` | `60` | Maximum expensive requests per principal per minute |
373
+ | `HARBORRAG_API_MAX_INFLIGHT_PER_PRINCIPAL` | `4` | Concurrent expensive request limit per principal |
374
+ | `HARBORRAG_API_REQUEST_TIMEOUT_SECONDS` | `120` | Server-owned wall-clock deadline for expensive requests |
375
+ | `HARBORRAG_CONTROL_DB_URL` | local SQLite runtime default | Control-plane SQLAlchemy URL |
376
+ | `HARBORRAG_INGESTION_TENANT_ID` | `DEFAULT` | Fallback tenant for non-HTTP runtime operations |
377
+ | `HARBORRAG_TEMPORAL_CONFIG_PATH` | `config/temporal.yaml` | Temporal connection, queues, retries, worker, ingestion, health, and workflow configuration |
378
+ | `HARBORRAG_TEMPORAL_TARGET` | value from YAML | Optional Temporal frontend override |
379
+ | `HARBORRAG_TEMPORAL_NAMESPACE` | value from YAML | Optional Temporal namespace override |
380
+ | `HARBORRAG_TEMPORAL_TLS` | value from YAML | Optional TLS override for a managed Temporal endpoint |
381
+ | `HARBORRAG_TEMPORAL_API_KEY` | unset | Secret API credential; keep it outside tracked YAML |
382
+ | `HARBORRAG_TEMPORAL_ALLOW_INSECURE_REMOTE` | `false` | Explicit opt-in for trusted plaintext remote targets |
383
+ | `HARBORRAG_TEMPORAL_INGESTION_BATCH_SIZE` | `200` | Default documents per `SourceBatchWorkflow` child; a CLI/API caller may override it per submission |
384
+ | `HARBORRAG_TEMPORAL_INGESTION_DOCUMENT_CONCURRENCY` | `8` | Default concurrent `DocumentIngestionWorkflow` children per wave; a CLI/API caller may override it per submission |
385
+ | `HARBORRAG_LOG_LEVEL` | `INFO` | HarborRAG namespace log level |
386
+
387
+ Credentialed CORS rejects wildcard origins. Swagger and the live OpenAPI route
388
+ default to disabled in production unless an operator explicitly enables them.
389
+
390
+ INFO logs report ingestion submission, discovery, document outcomes, failures,
391
+ and finalization. DEBUG additionally reports every Temporal activity boundary
392
+ and duration. Records include logger namespace, Python module, function, and
393
+ source line while omitting credentials and document/model content. See the
394
+ [troubleshooting guide](../../../../../docs/users/troubleshooting/README.md#an-accepted-ingestion-appears-to-do-nothing)
395
+ when an accepted task appears idle.
396
+
397
+ ## Errors and request tracing
398
+
399
+ Every non-success response uses one error envelope:
400
+
401
+ ```json
402
+ {
403
+ "error": {
404
+ "code": "harbor_validation_error",
405
+ "message": "Request validation failed",
406
+ "details": {},
407
+ "trace_id": "request-123"
408
+ }
409
+ }
410
+ ```
411
+
412
+ Send `X-Request-Id` to correlate a request with logs. The API echoes that value
413
+ on the response; when it is absent, middleware generates one. Unexpected
414
+ exceptions return a generic error while the complete traceback remains in
415
+ process logs.
416
+
417
+ ## OpenAPI contract
418
+
419
+ In development:
420
+
421
+ ```text
422
+ http://127.0.0.1:8000/api/v1/docs
423
+ http://127.0.0.1:8000/api/v1/openapi.json
424
+ ```
425
+
426
+ Export the deterministic contract without starting a server:
427
+
428
+ ```bash
429
+ uv run python -m harborrag_app.api.export_openapi > openapi.json
430
+ ```
431
+
432
+ ## Package boundary
433
+
434
+ The package is organized by responsibility:
435
+
436
+ ```text
437
+ api/
438
+ ├── app.py, router.py, settings.py, middleware.py, metrics.py
439
+ ├── auth/ # authentication and role dependencies
440
+ ├── routes/ # operational surface: health, metrics, projects,
441
+ │ # sources, activity, settings, deprecated routes
442
+ ├── schemas.py # contracts shared by API features
443
+ └── v1/ # the public /v1 contract
444
+ ├── admin/ # projection inspection and teardown
445
+ ├── agent/ # agent sessions, completions, run resume
446
+ ├── chat/ # chat sessions and completions
447
+ ├── connections/ # submittable connection listing
448
+ ├── ingestion/ # ingestion routes, schemas, and command mapping
449
+ └── retrieval/ # retrieval routes, schemas, and service dependency
450
+ ```
451
+
452
+ This package may:
453
+
454
+ - validate and serialize HTTP messages;
455
+ - authenticate principals and enforce route roles;
456
+ - manage API process lifecycle and middleware;
457
+ - delegate operations through `BaseAppService`.
458
+
459
+ It must not:
460
+
461
+ - instantiate connectors, parsers, model clients, or repositories;
462
+ - implement ingestion state transitions;
463
+ - expose Temporal SDK handles on the wire;
464
+ - return provider exception details or secrets.
465
+
466
+ Run its tests from the repository root:
467
+
468
+ ```bash
469
+ pytest packages/harborrag-app/tests/test_api_*.py
470
+ ```
File without changes
@@ -0,0 +1,193 @@
1
+ """FastAPI application factory for the HarborRAG Control Plane API (ST2).
2
+
3
+ `uvicorn harborrag_app.api.app:create_fastapi_app --factory` is the deploy
4
+ entrypoint (Dockerfile.api CMD). The factory wires settings, observability,
5
+ trace middleware, CORS, the error envelope, auth verifier, and versioned public
6
+ routers; the lifespan builds the app service through runtime composition.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import inspect
13
+ import logging
14
+ from collections.abc import AsyncIterator
15
+ from contextlib import asynccontextmanager
16
+
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.responses import RedirectResponse
20
+ from starlette.routing import compile_path
21
+
22
+ from harborrag_app import __version__
23
+ from harborrag_app.api.auth.dependencies import build_token_verifier
24
+ from harborrag_app.api.capacity import build_api_capacity_limiter
25
+ from harborrag_app.api.errors import register_error_handlers
26
+ from harborrag_app.api.metrics import ApiMetrics, ApiMetricsMiddleware
27
+ from harborrag_app.api.middleware import RequestBodyLimitMiddleware, TraceIdMiddleware
28
+ from harborrag_app.api.router import OPERATIONAL_PREFIX, register_routes
29
+ from harborrag_app.api.settings import ApiSettings
30
+ from harborrag_app.workflow_control.composition.selection import select_app_service
31
+ from harborrag_core.contracts.errors import HarborConfigurationError
32
+ from harborrag_core.observability.process_logging import configure_logging
33
+
34
+ logger = logging.getLogger("harborrag.app.api.app")
35
+ _SUBMISSION_RECOVERY_INTERVAL_SECONDS = 5.0
36
+ _INGESTION_PROGRESS_INTERVAL_SECONDS = 2.0
37
+ _CONTROL_PLANE_EFFECT_RECOVERY_INTERVAL_SECONDS = 30.0
38
+
39
+
40
+ def _redirect_root(request: Request) -> RedirectResponse:
41
+ """Send humans to docs when enabled, otherwise to the health endpoint."""
42
+ settings: ApiSettings = request.app.state.settings
43
+ destination = "docs" if settings.docs_enabled else "health"
44
+ return RedirectResponse(url=f"{OPERATIONAL_PREFIX}/{destination}")
45
+
46
+
47
+ def _redirect_docs() -> RedirectResponse:
48
+ """Preserve the familiar Swagger path without duplicating the API route."""
49
+ return RedirectResponse(url=f"{OPERATIONAL_PREFIX}/docs")
50
+
51
+
52
+ async def _recover_pending_submissions(app: FastAPI) -> None:
53
+ """Continuously drain the durable workflow-submission outbox."""
54
+
55
+ while True:
56
+ try:
57
+ recovered = await app.state.app_service.recover_pending_submissions()
58
+ if recovered:
59
+ logger.info("Recovered %d pending ingestion submissions", recovered)
60
+ except asyncio.CancelledError:
61
+ raise
62
+ except Exception:
63
+ logger.exception("Pending ingestion submission recovery pass failed")
64
+ await asyncio.sleep(_SUBMISSION_RECOVERY_INTERVAL_SECONDS)
65
+
66
+
67
+ async def _sync_ingestion_progress(app: FastAPI) -> None:
68
+ """Continuously fan active ingestion tasks' progress out via the event bus."""
69
+
70
+ while True:
71
+ try:
72
+ await app.state.app_service.sync_ingestion_progress()
73
+ except asyncio.CancelledError:
74
+ raise
75
+ except Exception:
76
+ logger.exception("Ingestion progress sync tick failed")
77
+ await asyncio.sleep(_INGESTION_PROGRESS_INTERVAL_SECONDS)
78
+
79
+
80
+ async def _recover_pending_control_plane_effects(app: FastAPI) -> None:
81
+ """Continuously drain the durable secret-retirement/audit-logging outbox."""
82
+
83
+ while True:
84
+ try:
85
+ recovered = await app.state.app_service.recover_pending_control_plane_effects()
86
+ if recovered:
87
+ logger.info("Recovered %d pending control-plane effects", recovered)
88
+ except asyncio.CancelledError:
89
+ raise
90
+ except Exception:
91
+ logger.exception("Pending control-plane effect recovery pass failed")
92
+ await asyncio.sleep(_CONTROL_PLANE_EFFECT_RECOVERY_INTERVAL_SECONDS)
93
+
94
+
95
+ @asynccontextmanager
96
+ async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
97
+ """Compose the app service on startup (ST8 selection rule).
98
+
99
+ Runs in a worker thread: production composition executes Alembic
100
+ migrations and a DB probe, which drive their own event loops.
101
+ """
102
+ service, mode = await asyncio.to_thread(select_app_service)
103
+ app.state.app_service = service
104
+ app.state.composition_mode = mode
105
+ logger.info("Application service composed in %s mode", mode)
106
+ recovery_task = asyncio.create_task(_recover_pending_submissions(app))
107
+ progress_task = asyncio.create_task(_sync_ingestion_progress(app))
108
+ control_plane_effect_recovery_task = asyncio.create_task(
109
+ _recover_pending_control_plane_effects(app)
110
+ )
111
+ try:
112
+ yield
113
+ finally:
114
+ recovery_task.cancel()
115
+ progress_task.cancel()
116
+ control_plane_effect_recovery_task.cancel()
117
+ await asyncio.gather(
118
+ recovery_task,
119
+ progress_task,
120
+ control_plane_effect_recovery_task,
121
+ return_exceptions=True,
122
+ )
123
+ logger.info("Closing the application service")
124
+ try:
125
+ close = getattr(service, "aclose", None)
126
+ if close is not None:
127
+ result = close()
128
+ if inspect.isawaitable(result):
129
+ await result
130
+ finally:
131
+ await app.state.api_capacity_limiter.aclose()
132
+
133
+
134
+ def create_fastapi_app(settings: ApiSettings | None = None) -> FastAPI:
135
+ """Build the Control Plane API app from (env-derived) settings."""
136
+ settings = settings or ApiSettings()
137
+ # uvicorn configures the root logger, not the "harborrag" namespace; this
138
+ # attaches the namespace handler so runtime and route logs are emitted.
139
+ configure_logging()
140
+ app = FastAPI(
141
+ title="HarborRAG Control Plane API",
142
+ version=__version__,
143
+ openapi_url=(f"{OPERATIONAL_PREFIX}/openapi.json" if settings.docs_enabled else None),
144
+ docs_url=f"{OPERATIONAL_PREFIX}/docs" if settings.docs_enabled else None,
145
+ redoc_url=None,
146
+ lifespan=_lifespan,
147
+ )
148
+ app.state.settings = settings
149
+ app.state.token_verifier = build_token_verifier(settings)
150
+ redis_url = (
151
+ settings.api_capacity_redis_url.get_secret_value()
152
+ if settings.api_capacity_redis_url is not None
153
+ else None
154
+ )
155
+ app.state.api_capacity_limiter = build_api_capacity_limiter(
156
+ redis_url=redis_url,
157
+ requests_per_minute=settings.api_requests_per_minute,
158
+ max_inflight=settings.api_max_inflight_per_principal,
159
+ lease_seconds=settings.api_request_timeout_seconds + 5,
160
+ )
161
+ app.state.api_metrics = ApiMetrics(version=app.version)
162
+ app.add_middleware(
163
+ RequestBodyLimitMiddleware,
164
+ max_body_bytes=settings.max_request_body_bytes,
165
+ )
166
+ app.add_middleware(ApiMetricsMiddleware, metrics=app.state.api_metrics)
167
+ app.add_middleware(TraceIdMiddleware)
168
+ if settings.cors_origins:
169
+ if "*" in settings.cors_origins:
170
+ raise HarborConfigurationError(
171
+ "wildcard cors_origins is not allowed with credentialed CORS; list explicit origins"
172
+ )
173
+ app.add_middleware(
174
+ CORSMiddleware,
175
+ allow_origins=settings.cors_origins,
176
+ allow_credentials=True,
177
+ allow_methods=["*"],
178
+ allow_headers=["*"],
179
+ )
180
+ register_error_handlers(app)
181
+ app.add_api_route("/", _redirect_root, methods=["GET"], include_in_schema=False)
182
+ if settings.docs_enabled:
183
+ app.add_api_route(
184
+ "/docs",
185
+ _redirect_docs,
186
+ methods=["GET"],
187
+ include_in_schema=False,
188
+ )
189
+ register_routes(app)
190
+ app.state.api_metric_routes = tuple(
191
+ (path, compile_path(path)[0]) for path in app.openapi()["paths"]
192
+ )
193
+ return app
@@ -0,0 +1 @@
1
+ """Bearer-token auth for the Control Plane API (ST4)."""