Files
2026-09-15 14:37:29 +02:00

419 lines
24 KiB
Markdown

# ThothII
ThothII is a human-reviewed NL-to-SQL workflow with a React frontend and a Fastify/Pi/`tht`
core. The portable deployment runs two application services plus the installation-local metadata
catalog; DWH and LLM services remain external. Semantic services are bundled in Compose.
The same frontend supports **full** (its own header) and **embedded** (inside a
portal). This choice is independent of authentication: the Mac uses full/local,
Omics uses embedded/upstream with its existing login, and a standalone server
can use full/OIDC. See [rendering architecture](docs/architecture/application-shell.md)
and [configuration, Omics delivery and deploy](docs/operations/shell-and-localization.md).
For the current server upgrade with Omics Portal, follow the ordered
[Codex server handoff](docs/operations/server-codex-handoff.md), including source
integration, embedded/upstream configuration, coordinated rollout and rollback.
Local/OIDC authentication is configured through the host CLI `tht`; portal
authentication is established by the trusted server proxy. See the
[local guide](docs/install/authentication-local.md),
[OIDC guide](docs/install/authentication-oidc.md),
[upstream integration](docs/install/authentication-upstream.md), and
[manual acceptance matrix](docs/testing/authentication-manual-acceptance.md).
For the clone-based manual standalone installation test on macOS, Windows, and Linux, use the
[Italian procedure](docs/install/standalone-manual-it.md) or the
[English procedure](docs/install/standalone-manual-en.md).
The [public manual](https://git.tylconsulting.it/thothii-docs/) covers the product,
installation, use and administration. Developer architecture, contracts, ADRs, tests,
plans and release records remain in this repository but are excluded from MkDocs
pages and search. This is an editorial boundary, not an access restriction on the
public repository. See the [documentation cleanup review](docs/maintenance/2026-09-15-documentation-cleanup.md)
for the executed consolidation and the inventory of historical sources retained in Git.
## Docker Compose and installation
For a fresh installation, follow the complete manual procedure in
[Italian](docs/install/standalone-manual-it.md) or
[English](docs/install/standalone-manual-en.md). Configure protected files first;
then run the documented build, explicit migrations and startup commands with the
same installation descriptor and Compose project. There is no installer or launcher.
The mandatory stack includes frontend, core, PostgreSQL catalog, Qdrant, Ollama
and the embedding initializer. DWH and LLM endpoints remain external dependencies.
Pi is included in the core image. Credentials and certificates belong in protected
installation-local files, never in the workspace repository.
For developer topology, overlays and lifecycle details, see the internal
[Compose reference](docs/operations/compose-reference.md). Ordinary stop/down keeps
persistent data; removing volumes is destructive and is not an upgrade step.
Process health is distinct from external dependency checks performed by doctor.
## Git-backed workspace repository
Workspace descriptors are shared through a validated Git repository while endpoint bindings and
secret files remain installation-local. The supported operating sequence is documented in
[Workspace operations](docs/operations/workspaces.md); it covers curator publication, installation
activation, runtime bindings, and preprocessing. The host setup and lifecycle path is in
[Install and first start](docs/install/first-start.md).
The curator-owned repository layout is:
```text
thoth-workspaces.yaml
<id>/workspace.yaml
<id>/evidence/**
```
`thoth-workspaces.yaml` uses the `schema_version` value `1` and the ordered `workspaces` list of
`{id, name, description?}` entries. It is authoritative for workspace ID, name, description, and
display order. Every catalog entry must have a matching descriptor in the same commit; otherwise
the complete candidate is rejected. Descriptors remain curator-owned and change only through a
Git commit and push from a separate authoring clone, followed by an installation pull. ThothII
never writes any workspace repository content.
The operator workflow is: curate catalog/descriptor/Evidence changes in Git, commit and push,
**Update workspace repository** from each ThothII installation, select the workspace, complete its
write-only runtime-secret fields, run **Validate workspace source** and **Test workspace
connections**, then select the workspace locally before creating sessions. Each new session
pins the Git revision it used; a later pull cannot change a Resume. Snapshot cleanup retains every
revision referenced by an open, closed, or failed unarchived session. It reconciles from the
single local installation list or from a server administrator's complete session list, never from
a remote user's partial list. The isolated deployment exercise is
`./scripts/workspace-registry-smoke.sh`; both manuals are checked with
`./scripts/verify-workspace-install-docs.sh --profile local` or `--profile server`.
<!-- workspace-descriptor-contract:start -->
Schema v4 is the only accepted workspace descriptor. It contains workspace identity and optional
Evidence configuration only; PostgreSQL Metadata Catalog owns every database fact and binding.
Schema v1, v2, and v3 descriptors are rejected before activation. Candidate snapshot validation
therefore makes activation or a pull fail atomically while the prior valid snapshot remains active.
Each workspace owns separate Qdrant `reference` and `memory` collections: Schema, relationships, and
Evidence are replaceable reference data; Memory and solved questions have a persistent lifecycle.
<!-- workspace-descriptor-contract:end -->
<!-- non-workspace-migration:start -->
Create a clean v4 descriptor containing only `workspace` and optional `evidence`. Do not copy the
legacy database, diagnostics, `llm_policy`, or `semantic_index` blocks; configure the database in
Database Management.
<!-- non-workspace-migration:end -->
For NL→SQL runtime sessions, connector `ssh_tunnel` bindings remain diagnostic-only: their bounded
probe cleans up the loopback forward and returns `workspace_not_activatable`; session creation is
rejected before persistence. Database management is a separate boundary and supports a strict
OpenSSH tunnel for **Test connection** and **Sync tables**, using a private key, optional passphrase,
mandatory `known_hosts`, and optional PostgreSQL TLS CA/server name. Git registry access over SSH is
unaffected. Use direct or REST connector transport for runtime sessions.
`docker-compose.dev.yml` is deliberately local: both published ports bind to `127.0.0.1`,
`THT_SESSION_STORAGE=local`, and `THT_HOME=/data/local-home`. Do not set
`THOTH_PUBLIC_EXPOSURE=true` for that profile; the backend rejects that public/local combination
at startup.
Run the end-to-end packaging check with:
```sh
./scripts/docker-smoke.sh
```
The smoke script validates Compose, builds and waits for both services, checks health through
the frontend, verifies SSE response headers, restarts the core, and confirms `/data` survives.
Each run uses a unique Compose project and removes that project's containers, network, and test
volume afterward. It never targets the fixed `thothii` operator project or its volume. Set
`SMOKE_PROJECT` to a different explicit project name for reproducible debugging, and set
`KEEP_SMOKE_RESOURCES=1` to retain that smoke project's resources for inspection; remove them
later with `docker compose --project-name "$SMOKE_PROJECT" down --volumes`.
## Unified deployment release gates
Task 13 adds no-secret release gates around the canonical local and server Compose profiles. Its
deterministic safety check does not contact the Docker daemon:
```sh
bash scripts/unified-deployment-smoke.sh --self-test
```
The three Linux Docker smokes are separate release commands. Each creates a unique Compose project, temporary
Git workspace remote, fixture provider, image names, and run label. Its exit trap removes only
resources carrying that exact run identity and never performs a global Docker prune. Cleanup
enumerates running and stopped project containers immediately before `compose down` and refuses
the teardown if any container, volume, or network has a foreign run label.
```sh
bash scripts/unified-deployment-smoke.sh
bash scripts/tht-update-smoke.sh
bash scripts/server-deployment-smoke.sh
```
The unified smoke builds and starts `frontend` and `core`, verifies the embedded Pi and internal
registry, recreates with the Git remote offline, activates a valid Git update, rejects invalid Git
content while retaining the valid snapshot, and checks the four persistence volumes. The unified
and update-only smokes
inject a digest-pinned non-core candidate under a deliberately mismatched Pi version and require
`tht pi update` to roll back while preserving settings, sessions, Pi state, registry revision,
and mount identity. The rollback candidate is the digest-pinned `hello-world` executable: a
preflight proves that it exits successfully, so the failed replacement core satisfies
`tht`'s stopped-core compensation precondition. The server smoke uses the same smoke-built
core/frontend images with the server and required session overlays, disposable bind roots and
secret files, upstream-auth checks, and a fail-closed `503` assertion for its deliberately
unavailable disposable session endpoint. No real provider, database credential, or repository
secret is required.
For a clean server bind, `scripts/prepare-server-pi-state.sh` creates the hidden regular Pi agent
mount targets atomically before Compose. The auth target receives the protected credential bind;
the model and settings targets receive generated read-only projections. The server smoke starts
from an empty Pi-state root and applies this same preflight. Deterministic fixture tests render
both profiles, verify that bindings stay on `core`, check mount readability, and run the production
workspace resolver. Wrong-service, wrong-value, and broken-secret-mount mutations must fail.
Each public smoke has its own 30-minute process-group supervisor with TERM/KILL cleanup; CI retains
an independent 32-minute outer timeout and does not retry a failed command.
Current release status (2026-08-05): clean-root render/setup and the production runtime-binding
resolver contracts are green. The server fixture supplies all four private trusted claims,
including exact non-admin value `0`, and a focused test proves nginx normalization produces the
accepted non-admin backend principal. Canonical schema-v4 registry descriptors now pass through
one backend-owned, secret-safe runtime handoff for inventory and session execution; canonical
identity and durable session/artifact/index roots are retained. The fresh update-only smoke passed
bad-candidate mutation, automatic `rolled_back` compensation, exact prior-image restoration,
unchanged registry/mount identity, all four sentinels, post-rollback doctor/workspace checks, and
exact cleanup. The one authorized server-smoke invocation was denied access to the Docker socket
by its execution sandbox before startup, so the complete authenticated workspace and fail-closed
session assertions still require a fresh authorized release run. Native Windows Docker
Desktop/WSL2 remains a separate manual/self-hosted gate.
The deterministic native Windows contract is:
```powershell
.\scripts\test-windows-clone-contract.ps1
```
It checks Git's CRLF/LF attributes and bytes, copies tracked source into a temporary path containing
spaces, builds and invokes native Windows `tht` there, and renders exactly `core` plus
`frontend` without starting containers. On a supported self-hosted Windows Docker Desktop/WSL2
runner, dispatch the deployment workflow with `windows_docker_startup=true`; that job executes:
```powershell
.\scripts\test-windows-clone-contract.ps1 -DockerStartup
```
Startup mode adds bounded image build/two-service health startup, installation-aware `tht`
status, stopped-container-aware ownership checks, and exact cleanup. The ordinary hosted Windows
job remains deterministic and does not claim Docker startup.
## Workspace preprocessing and S3 Evidence
For an interactive run, select the workspace, expand **Administration** in the right sidebar, and
use its **Preprocessing** control. The control explains any unmet prerequisite and exposes only the
latest safe failure diagnostic. For unattended operation, use the native host CLI and installation
descriptor:
```sh
tht --installation /absolute/path/thothii-installation.yaml \
workspace preprocess run --workspace <workspace-id>
tht --installation /absolute/path/thothii-installation.yaml \
workspace preprocess clear --workspace <workspace-id>
```
The one-shot command starts the profile-gated `workspace-maintenance` service, reads database
metadata from PostgreSQL, and rebuilds LSH plus schema/Evidence vectors. The clear command removes
those derived artifacts while preserving the separate Memory collection. The core remains unavailable
until preprocessing completes. See [Evidence](docs/evidence.md) and the
[workspace preprocessing CLI contract](docs/contracts/workspace-preprocessing-cli.md).
S3 Evidence uses the optional `tht[s3]` dependency and canonical `s3://bucket/key` provenance.
AWS endpoints are used when no custom URL is supplied. Every custom endpoint is an explicit egress
trust-boundary opt-in and uses path-style addressing; private and HTTP endpoints require additional
independent opt-ins. Literal non-global IPv4/IPv6 addresses are classified locally; hostnames are
not DNS-pinned, so trusted custom-endpoint deployments must enforce their destination with network
egress policy. Store access key, secret key, and session token as secret references in
deployment configuration—never in Compose environment values or source URIs. Discovery and reads
are bounded by configured page, object, and byte limits.
Create a versioned Qdrant volume backup for one exact Compose project (the filename is
operator-controlled, so use an immutable timestamp or release identifier):
```sh
./scripts/vector-backup.sh \
--project-name thothii \
--output /secure/backups/thoth-qdrant-2026-08-08.tar
```
The script resolves exactly one Docker volume with the labels
`com.docker.compose.project=<project>` and `com.docker.compose.volume=qdrant-data`, stops the
`qdrant` service if it is running, archives that volume's persistent contents, then restores the
prior service state. It never performs global Docker cleanup and refuses to overwrite an existing
archive path.
Restore targets that same exact project-scoped `qdrant-data` volume. Because restore replaces the
persistent Qdrant data in place, it requires an explicit confirmation that exactly repeats the
Compose project name by passing `--confirm-project`:
```sh
./scripts/vector-restore.sh \
--project-name thothii \
--input /secure/backups/thoth-qdrant-2026-08-08.tar \
--confirm-project thothii
```
The restore script stops `qdrant`, validates the exact labeled target, stages the current volume
contents for rollback, extracts the requested archive into the volume, and then returns the
service to its prior running state. It restores semantic storage only. Before reopening write
traffic, the workspace registry must already be at a reviewed v4 descriptor revision compatible
with the restored collection; then run backend health checks and a known retrieval query. The
helper does not restore descriptors, rename collections, or reconcile an incompatible collection
contract.
## Production trust boundary and secrets
ThothII does not implement OIDC. Do not expose its application port directly to a network.
The production pattern is an authenticated host reverse proxy that:
- terminates TLS and authenticates every request;
- removes any client-supplied identity header;
- injects one trusted `X-Authenticated-User` value;
- proxies to the loopback-only ThothII frontend.
[`deploy/nginx-authenticated-proxy.conf.example`](deploy/nginx-authenticated-proxy.conf.example)
shows the contract using nginx `auth_request`; replace the placeholder authentication gateway
with the organization's reviewed identity proxy. `AUTH_MODE=upstream` trusts this boundary and
rejects requests without the identity header. Setting `THOTH_PUBLIC_EXPOSURE=true` with any other
auth mode fails during core startup.
Production credentials use the existing Compose secret-bundle contract, never environment values.
Copy `deploy/secrets/thothii.secrets.example` to a protected host file, include only the required
keys, and set its absolute path as `THT_SECRETS_FILE` in the operator env. Keep Pi's native
provider auth in the separate protected file named by `PI_AUTH_FILE`.
Interactive sessions, Description Generation, and embedding share the protected installation
descriptor's `modelCatalog`. Set `THT_INSTALLATION_CONFIG_SOURCE` to that exact host file; `tht`
validates it and generates the runtime catalog, Pi adapters, and Compose override before startup.
Each authenticated provider stores only an audited `apiKeyEnv` reference; the referenced value stays
in the secret bundle. A provider may use `authentication.mode: none` only with an explicit keyless
endpoint. The browser receives only eligible model IDs, labels, and the catalog default.
Before enabling Description Generation, approve the selected model provider for bounded source-data
disclosure. Every catalog column has a **Sensitive** flag that defaults to `false`. Administrators can
request an AI proposal based only on structural metadata, then must review and save the resulting
checkboxes themselves. The proposal never reads column contents and is not persisted automatically.
For unprotected columns, a request may send up to five real source rows and five representative
distinct, non-null example values. Protected columns are omitted from source reads and replaced in the
prompt by deterministic plausible values derived only from column metadata. Samples are transient and
are not stored in generation runs, run logs, application logs, API responses, or catalog metadata;
prompt and sample snapshots are not retained. A flag change applies to later generations and does not
regenerate existing descriptions.
Description Generation is an interactive Database Management operation, not a user-facing CLI.
The installation runs at most one sequential generation at a time. The run drawer exposes safe
ordered events through SSE with polling fallback, Stop terminates the current helper while keeping
already stored results, and Run history retains terminal runs for inspection. A backend restart
marks queued or running work interrupted instead of resuming it; use Generate Missing to continue.
Unlock is reserved for a stale recorded run and is rejected while a local start, worker, or helper
is still live.
The bundle is mounted read-only as `/run/secrets/thothii.secrets` and must be mode `0600` or
`0400` on the host. Docker's runtime `0444` mode is accepted only beneath `/run/secrets`; see
[`deploy/secrets/README.md`](deploy/secrets/README.md). A PEM CA chain is deliberately not a
bundle value: PEM contains whitespace and is rejected by the strict parser. Keep the CA chain in
the host/secret-manager materialization and add a reviewed Compose override that mounts it at
`/run/secrets/ca-chain.pem` and sets `THT_SSL_CA` when a private CA is required. The base bundle
does not create that mount. The frontend remains on loopback; the authenticated host proxy is the
only public listener.
For each Pi spawn, the backend resolves the selected canonical provider/model in the runtime catalog,
reads exactly that provider's declared `apiKeyEnv` value from the bundle, and exposes only that key
to the child. Ambient provider credentials and secret-bundle paths are scrubbed. Providers needing a
compound credential bundle remain unsupported until the catalog gains an explicit generic contract
for them.
## User-owned session server cutover
The server profile stores sessions and per-user preferences directly in PostgreSQL schema
`thoth_sessions`; it does not use PostgREST, browser storage, a shared session directory, or a
dual write. Use [`deploy/compose.session-server.yaml.example`](deploy/compose.session-server.yaml.example)
with the canonical base+server files and set `THT_SERVER_WORKSPACE_CONFIG` to an absolute,
protected copy of [`deploy/workspaces/server-sessions.yaml.example`](deploy/workspaces/server-sessions.yaml.example).
That file is an installation runtime template, not an authored workspace descriptor; database
bindings are injected from the PostgreSQL Metadata Catalog for each runtime lease.
The runtime login needs membership in the no-login database role `thoth_sessions_runtime` only.
The distinct, one-shot migrator login needs migration authority and uses
`thoth_sessions_migrator`; it must never be mounted into `core`. Set the non-secret endpoint and
role fields in the protected deployment environment:
```dotenv
AUTH_MODE=upstream
THOTH_PUBLIC_EXPOSURE=true
THT_SESSION_STORAGE=postgres
THT_SESSION_DB_HOST=sessions-db.internal
THT_SESSION_DB_PORT=5432
THT_SESSION_DB_NAME=thoth
THT_SESSION_RUNTIME_USER=thoth_sessions_app
THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate
THT_SESSION_DB_SSLMODE=verify-full
THT_SESSION_RUNTIME_PASSWORD_SOURCE=/secure/thoth/session-runtime-password
THT_SESSION_MIGRATOR_PASSWORD_SOURCE=/secure/thoth/session-migrator-password
THT_SESSION_CA_SOURCE=/secure/thoth/session-ca.pem
```
The overlay mounts the runtime password at `/run/secrets/session_runtime_password`, the CA at
`/run/secrets/session_ca.pem`, and passes those paths—not their contents—to the server workspace.
It mounts `session_migrator_password` only to `session-migrate`. The backend refuses a server
session store without upstream authentication, direct DB host/name/runtime user/password-file,
`verify-ca` or `verify-full`, and an absolute CA path.
The migrator independently rejects every other TLS mode before reading its password secret or
constructing a database URL.
Perform the cutover in one maintenance window, with the upstream identity-proxy headers and
backend principal parser deployed together. Neither change is safe to deploy independently: the
proxy clears the legacy identity header and the backend rejects it. Drain/stop active Pi work,
enable a maintenance response at the proxy, then run the migrator once and inspect its pristine JSON:
```sh
docker compose --env-file deploy/env/server.env \
-f compose.yaml -f deploy/compose.server.yaml -f deploy/compose.session-server.yaml.example \
--profile session-migrate run --rm session-migrate
```
It must report no pending or drifted migrations before starting the replacement core. `/health`
is a liveness probe and remains `200`; any request that needs unavailable repository storage
returns a fixed `503` before a Pi process starts. Verify this with an authenticated request after
the replacement core is healthy, then remove maintenance mode.
Do not import the three legacy server filesystem sessions: they have no trusted owner binding.
During the same maintenance window, archive the exact three reviewed IDs, verify the generated
archive and `.sha256`, then rerun the command with `--delete` to remove only those three source
directories:
```sh
./docker/cutover-legacy-sessions.sh \
/secure/thoth/legacy-sessions /secure/backups/thoth-legacy-sessions-2026-07-16.tar \
SESSION_ID_1 SESSION_ID_2 SESSION_ID_3
# After independent archive review, use a new backup filename:
./docker/cutover-legacy-sessions.sh --delete \
/secure/thoth/legacy-sessions /secure/backups/thoth-legacy-sessions-2026-07-16-delete.tar \
SESSION_ID_1 SESSION_ID_2 SESSION_ID_3
```
The helper refuses to overwrite an existing backup and refuses any count other than three
distinct IDs. Never run it against a live path without the maintenance gate. Roll back application
code only by keeping PostgreSQL as the single source of truth and deploying a compatible fixed
release. Do not restore filesystem persistence, do not re-import the archive, and never dual-write
sessions to database and files.
## Reproducible image verification
Base images use exact tags and immutable multi-platform manifest digests. Dependency update and
residual OS-repository limitations are documented in [`docker/LOCKS.md`](docker/LOCKS.md).
Run the shared architecture gate with `PLATFORM=linux/amd64` or `PLATFORM=linux/arm64`:
```sh
PLATFORM=linux/arm64 ./scripts/verify-container-images.sh
```
It builds both images, runs common version/runtime/security smokes, and emits an image/package
inventory beneath `.artifacts/container-images/`. CI runs the same script for both architectures.