From 72e16dd5ea2bf6bc22f9d87c7ee6db23a6a3051f Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 4 Aug 2026 07:57:09 +0200 Subject: [PATCH] docs: add workspace registry installation manuals --- README.md | 9 + .../local-compose.workspace-registry.yaml | 46 +++++ .../server-compose.workspace-registry.yaml | 47 +++++ docs/install/local-workspace-registry.md | 185 +++++++++++++++++ docs/install/server-workspace-registry.md | 195 ++++++++++++++++++ scripts/verify-workspace-install-docs.sh | 102 +++++++++ 6 files changed, 584 insertions(+) create mode 100644 docs/install/examples/local-compose.workspace-registry.yaml create mode 100644 docs/install/examples/server-compose.workspace-registry.yaml create mode 100644 docs/install/local-workspace-registry.md create mode 100644 docs/install/server-workspace-registry.md create mode 100755 scripts/verify-workspace-install-docs.sh diff --git a/README.md b/README.md index 7d23e7fa..a515f541 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,15 @@ The frontend depends on the core health check and proxies `/health` and `/api/*` application health endpoint intentionally checks process readiness only; external dependency diagnostics are exposed by `tht doctor` and do not prevent the UI from starting. +## Git-backed workspace registry + +Workspace descriptors are shared through a validated Git repository while endpoint bindings and +secret files remain installation-local. Use the [local Mac/PC installation manual](docs/install/local-workspace-registry.md) +for Docker Desktop or a local engine, and the [server installation manual](docs/install/server-workspace-registry.md) +for the Gitea, reverse-proxy, backup, migration, and recovery workflow. 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`. + `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 diff --git a/docs/install/examples/local-compose.workspace-registry.yaml b/docs/install/examples/local-compose.workspace-registry.yaml new file mode 100644 index 00000000..a54c008f --- /dev/null +++ b/docs/install/examples/local-compose.workspace-registry.yaml @@ -0,0 +1,46 @@ +# Standalone local registry example. Copy beside the clone as compose.workspace-registry.yaml +# and put path-only bindings in .env; keep the referenced files outside Git. +name: thothii-workspace-registry-local + +services: + core: + image: thothii-core:local + build: + context: ../../.. + dockerfile: docker/core.Dockerfile + environment: + HOST: 0.0.0.0 + PORT: "8787" + AUTH_MODE: none + THT_SESSION_STORAGE: local + THT_HOME: /data/local-home + SETTINGS_FILE: /data/settings/settings.json + THT_HARNESS_DIR: /app/harness + THT_BIN: /opt/venv/bin/tht + THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry + THT_WORKSPACE_GIT_REMOTE: ${THT_WORKSPACE_GIT_REMOTE:-ssh://git@git.example.invalid/platform/thoth-workspaces.git} + THT_WORKSPACE_GIT_BRANCH: ${THT_WORKSPACE_GIT_BRANCH:-main} + THT_WORKSPACE_INSTALLATION_ID: ${THT_WORKSPACE_INSTALLATION_ID:-local-laptop} + THT_WORKSPACE_GIT_AUTHOR_NAME: ${THT_WORKSPACE_GIT_AUTHOR_NAME:-Thoth Workspace Registry} + THT_WORKSPACE_GIT_AUTHOR_EMAIL: ${THT_WORKSPACE_GIT_AUTHOR_EMAIL:-thoth-workspace-registry@localhost} + THT_WORKSPACE_SECRET_ROOTS: /run/secrets + GIT_CONFIG_COUNT: "2" + GIT_CONFIG_KEY_0: credential.helper + GIT_CONFIG_VALUE_0: store --file=/run/secrets/workspace-registry-git-credentials + GIT_CONFIG_KEY_1: http.sslCAInfo + GIT_CONFIG_VALUE_1: /run/secrets/workspace-registry-git-ca + GIT_SSH_COMMAND: ssh -i /run/secrets/workspace-registry-git-ssh-key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/run/secrets/workspace-registry-git-known-hosts + ports: + - "127.0.0.1:8787:8787" + volumes: + - thoth-local-data:/data + - workspace-registry:/data/workspace-registry + - ${THT_WORKSPACE_GIT_CREDENTIALS_FILE:-./installation-secrets/git-credentials}:/run/secrets/workspace-registry-git-credentials:ro + - ${THT_WORKSPACE_GIT_CA_FILE:-./installation-secrets/git-ca.pem}:/run/secrets/workspace-registry-git-ca:ro + - ${THT_WORKSPACE_GIT_SSH_KEY_FILE:-./installation-secrets/git-ssh-key}:/run/secrets/workspace-registry-git-ssh-key:ro + - ${THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE:-./installation-secrets/git-known-hosts}:/run/secrets/workspace-registry-git-known-hosts:ro + restart: "no" + +volumes: + thoth-local-data: {} + workspace-registry: {} diff --git a/docs/install/examples/server-compose.workspace-registry.yaml b/docs/install/examples/server-compose.workspace-registry.yaml new file mode 100644 index 00000000..8ff23178 --- /dev/null +++ b/docs/install/examples/server-compose.workspace-registry.yaml @@ -0,0 +1,47 @@ +# Server registry example. Copy to a reviewed, untracked operator directory and set host paths +# and Git values in its .env. The core remains non-root (UID 10001) and never receives secrets +# through the Git checkout. +name: thothii-workspace-registry-server + +services: + core: + image: thothii-core:local + build: + context: ../../.. + dockerfile: docker/core.Dockerfile + environment: + HOST: 0.0.0.0 + PORT: "8787" + AUTH_MODE: upstream + THOTH_PUBLIC_EXPOSURE: "true" + THT_HARNESS_DIR: /app/harness + THT_BIN: /opt/venv/bin/tht + SETTINGS_FILE: /data/settings/settings.json + THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry + THT_WORKSPACE_GIT_REMOTE: ${THT_WORKSPACE_GIT_REMOTE:-ssh://git@git.example.invalid/platform/thoth-workspaces.git} + THT_WORKSPACE_GIT_BRANCH: ${THT_WORKSPACE_GIT_BRANCH:-main} + THT_WORKSPACE_INSTALLATION_ID: ${THT_WORKSPACE_INSTALLATION_ID:-production-1} + THT_WORKSPACE_GIT_AUTHOR_NAME: ${THT_WORKSPACE_GIT_AUTHOR_NAME:-Thoth Workspace Registry} + THT_WORKSPACE_GIT_AUTHOR_EMAIL: ${THT_WORKSPACE_GIT_AUTHOR_EMAIL:-thoth-workspace-registry@localhost} + THT_WORKSPACE_SECRET_ROOTS: /run/secrets + GIT_CONFIG_COUNT: "2" + GIT_CONFIG_KEY_0: credential.helper + GIT_CONFIG_VALUE_0: store --file=/run/secrets/workspace-registry-git-credentials + GIT_CONFIG_KEY_1: http.sslCAInfo + GIT_CONFIG_VALUE_1: /run/secrets/workspace-registry-git-ca + GIT_SSH_COMMAND: ssh -i /run/secrets/workspace-registry-git-ssh-key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/run/secrets/workspace-registry-git-known-hosts + volumes: + - ${THT_HOST_DATA_ROOT:-/srv/thothii/data}:/data + - ${THT_WORKSPACE_REGISTRY_HOST_PATH:-/srv/thothii/workspace-registry}:/data/workspace-registry + - ${THT_WORKSPACE_GIT_CREDENTIALS_FILE:-/srv/thothii/secrets/git-credentials}:/run/secrets/workspace-registry-git-credentials:ro + - ${THT_WORKSPACE_GIT_CA_FILE:-/srv/thothii/secrets/git-ca.pem}:/run/secrets/workspace-registry-git-ca:ro + - ${THT_WORKSPACE_GIT_SSH_KEY_FILE:-/srv/thothii/secrets/git-ssh-key}:/run/secrets/workspace-registry-git-ssh-key:ro + - ${THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE:-/srv/thothii/secrets/git-known-hosts}:/run/secrets/workspace-registry-git-known-hosts:ro + networks: + - portal + restart: unless-stopped + +networks: + portal: + external: true + name: ${THT_PORTAL_NETWORK:-omics_portal_omics_network} diff --git a/docs/install/local-workspace-registry.md b/docs/install/local-workspace-registry.md new file mode 100644 index 00000000..31b2f04c --- /dev/null +++ b/docs/install/local-workspace-registry.md @@ -0,0 +1,185 @@ +# Local workspace-registry installation (Mac and PC) + +This guide runs a single-user ThothII registry on Docker Desktop (macOS or Windows) or a local +Linux Docker Engine. It is intentionally loopback-only. Git is shared; the checkout, connector +bindings, credentials, and session data are local. Never put credentials in workspace YAML, Git, +browser drafts, diagnostics, or `.env.example`. + +## Prerequisites + +- macOS: Docker Desktop, Git, and sufficient volume disk space. Git Credential Manager is useful + for HTTPS sign-in. +- Windows: Docker Desktop with WSL2, Git for Windows, and the clone enabled in Docker file sharing. + Use absolute paths/WSL paths; PowerShell uses `;` rather than `:` in `COMPOSE_FILE`. +- Linux PC: Docker Engine, Compose plugin, Git, and a user permitted to run Docker. +- Outbound access to the Git remote. A local installation needs no inbound firewall rule. + +Keep the operator `.env` and `installation-secrets/` outside the workspace-registry Git checkout. +On macOS/Linux use mode `0600` for individual secret files. On Windows apply an ACL that grants +read access only to the Docker Desktop user. Do not use an empty file to silently bypass a selected +authentication method. + +## Git remote: SSH and HTTPS + +Create one private repository such as `thoth-workspaces.git`. It contains canonical workspace +definitions and generated artifacts only: + +```text +thoth-workspaces.yaml +workspaces/.yaml +workspaces/.env.example +workspaces/.md +``` + +For SSH, use a scoped deploy key, a verified `known_hosts` file, and strict host-key checking. For +HTTPS, use Git Credential Manager or a secret-manager-created credentials file. Mount a private +HTTPS CA as its own file. Do not disable host or certificate verification. + +```dotenv +THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git +THT_WORKSPACE_GIT_BRANCH=main +THT_WORKSPACE_INSTALLATION_ID=local-laptop +THT_WORKSPACE_GIT_SSH_KEY_FILE=/absolute/path/installation-secrets/git-ssh-key +THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=/absolute/path/installation-secrets/git-known-hosts +THT_WORKSPACE_GIT_CA_FILE=/absolute/path/installation-secrets/git-ca.pem +``` + +For HTTPS set `THT_WORKSPACE_GIT_CREDENTIALS_FILE` instead of the SSH key/known-hosts pair. Remote +and branch are non-secret; every `*_FILE` is a local path whose content never enters Git or logs. + +## Shared Git values, local bindings, and secret files + +| Location | Contains | Never contains | +| --- | --- | --- | +| Git workspace repository | schema v2 YAML, generated binding names, LLM policy, model/index identity | installation hostnames, keys, passwords, certificates, SSH keys | +| local `.env` | remote, branch, installation ID, selected transport and endpoints | secret contents | +| local secret directory | Git credentials/key, known hosts, CA, connector secret files | a copied registry checkout | +| Docker volumes | registry checkout/snapshots/state/locks and local data | host-only secret source files | + +The persistent volume is `/data/workspace-registry`: + +```text +repo/ # persistent Git checkout +snapshots/ # immutable validated revisions used by sessions +state/ # active revision and registry state +locks/ # short-lived publish locks +``` + +Installation variables are deterministic: `psd-clinical` becomes `PSD_CLINICAL`, and every name +is `THT_WS___`. Credentials and certificates use `*_FILE` path variables. +If declared, `THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE` is distinct from the vector reader +file; a reader credential is never repurposed for writing. + +## Direct PostgreSQL, REST, and SSH tunnel bindings + +Set only fields for the selected transport. Canonical YAML keeps database/schema/collection, +distance, embedding model, and dimensions shared in Git. + +```dotenv +# Direct PostgreSQL and pgvector +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct +THT_WS_PSD_CLINICAL_DWH_HOST=dwh.example.invalid +THT_WS_PSD_CLINICAL_DWH_PORT=5432 +THT_WS_PSD_CLINICAL_DWH_USER=thoth_reader +THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE=/run/secrets/psd-dwh-reader +THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT=pgvector_direct +THT_WS_PSD_CLINICAL_VECTOR_HOST=vector.example.invalid +THT_WS_PSD_CLINICAL_VECTOR_PORT=5432 +THT_WS_PSD_CLINICAL_VECTOR_USER=thoth_vector_reader +THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE=/run/secrets/psd-vector-reader +THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL=https://embeddings.example.invalid +``` + +```dotenv +# REST; an API-key file is needed only for a declared bearer/x-api-key diagnostic. +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api +THT_WS_PSD_CLINICAL_DWH_BASE_URL=https://dwh.example.invalid +THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE=/run/secrets/psd-dwh-api-key +THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT=rest_api +THT_WS_PSD_CLINICAL_VECTOR_BASE_URL=https://vectors.example.invalid +THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE=/run/secrets/psd-vector-api-key +``` + +```dotenv +# SSH tunnel; host-key verification and TLS target name remain mandatory. +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=ssh_tunnel +THT_WS_PSD_CLINICAL_DWH_USER=thoth_reader +THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE=/run/secrets/psd-dwh-reader +THT_WS_PSD_CLINICAL_DWH_SSH_HOST=bastion.example.invalid +THT_WS_PSD_CLINICAL_DWH_SSH_PORT=22 +THT_WS_PSD_CLINICAL_DWH_SSH_USER=thoth_tunnel +THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE=/run/secrets/psd-dwh-tunnel-key +THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE=/run/secrets/psd-dwh-known-hosts +THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST=dwh.internal.example +THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT=5432 +``` + +Repeat the SSH names for `VECTOR` where needed. REST diagnostics reject a private per-request CA +rather than weakening TLS; use runtime-trusted HTTPS or verified direct/SSH native TLS. See the +[diagnostic protocol](../workspace-diagnostic-protocol.md). + +## Bootstrap, first pull, and diagnostics + +Copy [the local Compose example](examples/local-compose.workspace-registry.yaml) into an untracked +operator directory, create its local `.env` and mounted secret files, then render it before start. + + +```sh +docker compose -f docs/install/examples/local-compose.workspace-registry.yaml config --quiet +``` + +From the operator directory: + +```sh +docker compose -f compose.workspace-registry.yaml up --build -d +curl --fail --silent http://127.0.0.1:8787/health +curl --fail --silent http://127.0.0.1:8787/workspace-registry/status +curl --fail --silent http://127.0.0.1:8787/workspaces +``` + +The first status request clones, validates all descriptors, and atomically activates a snapshot. +Use `POST /workspace-registry/pull` to fetch later revisions. Run workspace diagnostics only after +required bindings are mounted. The optional writer probe uses a distinct writer file and removes +its uniquely named temporary record; ordinary diagnostics are read-only. + +To migrate an existing PSD descriptor, create/clone an empty private remote, transform with +absolute paths, review the schema-v1 result, explicitly add vector database/schema and the complete +schema-v2 contract, then commit/push. The transformer never imports `${ENV}` values or secrets. + +```sh +npm --prefix backend run build +node backend/dist/workspaces/migrate-legacy.js --input /absolute/path/psd.yaml --output /absolute/path/thoth-workspaces +``` + +## Publish, update, backup, outage recovery, and rollback + +Drafts live only in browser storage. Review the canonical field diff, validate/test locally, then +publish. If conflicted, pull first and create a new reviewed field-level draft; never hand-edit the +running `repo/` volume. Before upgrading, record registry status, stop Compose, and take a +timestamped ownership-preserving backup of both registry and local data volumes while excluding +`installation-secrets/`. Render Compose, rebuild, start, and check status before resuming work. + +After a valid bootstrap, remote outage retains the last valid snapshot and reports `degraded: true`. +Pinned sessions continue. Repair network/authentication, pull, and confirm non-degraded status. To +undo a bad remote change, create a reviewed Git revert/release branch, advance the remote through +normal policy, pull, and confirm its new snapshot. Do not delete `snapshots/` as rollback. + +## Troubleshooting + +| Stable code | Meaning and safe action | +| --- | --- | +| `workspace_invalid` | Invalid descriptor/path/snapshot; restore a reviewed canonical revision. | +| `binding_missing` | A selected value or readable `*_FILE` is absent; fix the local binding/mount. | +| `workspace_not_activatable` | Diagnostics cannot activate the workspace; correct the selected transport. | +| `workspace_stale` | Checkout changed or is busy; stop competing pull/publish work. | +| `workspace_conflict` | Draft base differs from Git; pull, resolve the diff, validate, republish. | +| `git_unavailable` | Remote, path, network, or lock unavailable; preserve the degraded valid snapshot. | +| `git_auth_failed` | Mounted SSH/HTTPS material rejected/unreadable; rotate or fix permissions without logging it. | +| `git_non_fast_forward` | Checkout diverged; reconcile through the registry workflow. | +| `git_push_rejected` | Remote policy rejected the change; review branch protection/hooks. | +| `connector_unavailable` | DNS/TLS/auth/resource identity diagnostic failed; inspect local bindings and egress. | +| `semantic_index_incompatible` | Collection/model/dimensions/distance differs from Git; perform an explicit index migration. | + +On macOS, restart Docker Desktop if a named volume disappears. On Windows, check WSL2 and Docker +file sharing. A failed first bootstrap has no snapshot fallback: repair remote trust and retry; +never create an unreviewed local registry repository. diff --git a/docs/install/server-workspace-registry.md b/docs/install/server-workspace-registry.md new file mode 100644 index 00000000..d89cb99d --- /dev/null +++ b/docs/install/server-workspace-registry.md @@ -0,0 +1,195 @@ +# Server workspace-registry installation + +This is the production operator guide. The application image is read-only, secrets are mounted +read-only, and sessions use immutable Git-validated snapshots. Expose the application only behind +an authenticated same-origin reverse proxy; never publish the core port directly. + +## Service account, storage, and firewall + +Create a dedicated host service account and an operator root such as `/srv/thothii`. The core +container is non-root UID/GID `10001` (`thoth`), so give that identity read/write ownership before +first startup. Keep storage separated: + +```text +/srv/thothii/data/ # settings, session data, Pi state as applicable +/srv/thothii/workspace-registry/ # repo/, snapshots/, state/, locks/ +/srv/thothii/secrets/ # Git and connector secret files, mode 0700 +/srv/thothii/operator/ # untracked Compose/.env, mode 0700 +``` + +Permit outbound TCP only to approved Git/Gitea, DWH, vector, embedding, and bastion endpoints. +Allow inbound traffic only from the reverse proxy/Docker network. Do not give the runtime service +account Gitea administration, database-superuser rights, or a shell in the Git host. + +## Gitea and remote Git setup + +Create a private Gitea (or compatible Git) repository such as `platform/thoth-workspaces`. Protect +`main` according to the release policy and grant the ThothII publisher only the intended repository +scope. Commit canonical schema-v2 descriptors and generated `.md`/`.env.example` artifacts only; +do not commit installation bindings or secret material. + +For SSH, create a least-privilege deploy key, record Gitea's host key in managed known-hosts, and +use `ssh://git@git.example.invalid/platform/thoth-workspaces.git`. For HTTPS, create a scoped +machine credential in the secret manager and mount the Gitea/private CA separately. Never use a +Gitea admin credential in the application. + +Bootstrap an empty remote from a temporary review clone: migrate legacy descriptors, review their +schema-v2 identity and generated artifacts, commit, and push `main`. The running server is not an +authoring environment for migration. + +## Git credentials, CA, SSH key, and known-hosts mounts + +Use the secret manager or a protected host-only procedure to create independent regular files under +`/srv/thothii/secrets`. Set individual mode `0600`, directory mode `0700`, and ownership readable +by the service account. These path-only variables are mounted read-only by Compose: + +```dotenv +THT_WORKSPACE_GIT_CREDENTIALS_FILE=/srv/thothii/secrets/git-credentials +THT_WORKSPACE_GIT_CA_FILE=/srv/thothii/secrets/git-ca.pem +THT_WORKSPACE_GIT_SSH_KEY_FILE=/srv/thothii/secrets/git-ssh-key +THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=/srv/thothii/secrets/git-known-hosts +``` + +Use the credential file for HTTPS, or key and known-hosts for SSH. Strict host-key checking stays +enabled and Git stderr is not exposed by the API. Rotate by atomically replacing the secret file, +restarting `core`, and performing pull/status; never put the material in an environment variable or +`docker compose config` output. + +## Shared Git values, local bindings, and secret files + +Git describes workspace schema, immutable ID, DWH/vector identity, semantic-index dimensions and +distance, embedding contract, and LLM policy. The installation supplies remote/branch/installation +ID, transport, endpoints, users, ports, and `THT_WS_*` bindings. Secret contents are only in files, +never the values stored in Git or browser-local drafts. + +The runtime registry layout is persistent and must be backed up together: + +```text +/data/workspace-registry/repo/ +/data/workspace-registry/snapshots/ +/data/workspace-registry/state/ +/data/workspace-registry/locks/ +``` + +Variable names derive from the immutable ID: `psd-clinical` becomes `PSD_CLINICAL`, producing +`THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE`. A declared vector writer uses the distinct +`THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE`; a reader file is never a writer substitute. + +## Direct PostgreSQL, REST, and SSH tunnel bindings + +Select only a transport allowed by canonical YAML; preserve database/schema/collection, model, +dimensions, and distance as Git-shared identity. + +```dotenv +# Direct PostgreSQL/pgvector with verified native TLS if a CA path is supplied. +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct +THT_WS_PSD_CLINICAL_DWH_HOST=dwh.internal.example +THT_WS_PSD_CLINICAL_DWH_PORT=5432 +THT_WS_PSD_CLINICAL_DWH_USER=thoth_reader +THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE=/run/secrets/psd-dwh-reader +THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT=pgvector_direct +THT_WS_PSD_CLINICAL_VECTOR_HOST=vector.internal.example +THT_WS_PSD_CLINICAL_VECTOR_PORT=5432 +THT_WS_PSD_CLINICAL_VECTOR_USER=thoth_vector_reader +THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE=/run/secrets/psd-vector-reader +``` + +```dotenv +# REST needs API-key file paths only when the descriptor declares authenticated diagnostics. +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api +THT_WS_PSD_CLINICAL_DWH_BASE_URL=https://dwh.internal.example +THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE=/run/secrets/psd-dwh-api-key +THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT=rest_api +THT_WS_PSD_CLINICAL_VECTOR_BASE_URL=https://vectors.internal.example +THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE=/run/secrets/psd-vector-api-key +THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL=https://embeddings.internal.example +``` + +```dotenv +# SSH tunnel requires explicit host-key verification and TLS target identity. +THT_WS_PSD_CLINICAL_DWH_TRANSPORT=ssh_tunnel +THT_WS_PSD_CLINICAL_DWH_USER=thoth_reader +THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE=/run/secrets/psd-dwh-reader +THT_WS_PSD_CLINICAL_DWH_SSH_HOST=bastion.internal.example +THT_WS_PSD_CLINICAL_DWH_SSH_PORT=22 +THT_WS_PSD_CLINICAL_DWH_SSH_USER=thoth_tunnel +THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE=/run/secrets/psd-dwh-tunnel-key +THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE=/run/secrets/psd-dwh-known-hosts +THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST=dwh.internal.example +THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT=5432 +``` + +Repeat SSH variables for `VECTOR` when selected. REST diagnostics refuse private per-request CAs +rather than disable verification; use runtime-trusted HTTPS or verified direct/SSH native TLS. See +the [diagnostic protocol](../workspace-diagnostic-protocol.md) for its read-only checks and optional +reversible writer probe. + +## Same-origin reverse proxy, bootstrap, and health + +Copy [the server Compose example](examples/server-compose.workspace-registry.yaml) to the protected +operator directory, set host paths/remote/branch/installation ID/portal network in local `.env`, +then render it before deployment. + + +```sh +docker compose -f docs/install/examples/server-compose.workspace-registry.yaml config --quiet +``` + +Configure the portal proxy so the frontend and `/api` share one origin. It authenticates first and +forwards the trusted identity expected by `AUTH_MODE=upstream`; it is the only public listener. +From a trusted maintenance shell: + +```sh +docker compose -f compose.workspace-registry.yaml up --build -d +docker compose -f compose.workspace-registry.yaml exec -T core curl --fail --silent http://127.0.0.1:8787/health +docker compose -f compose.workspace-registry.yaml exec -T core curl --fail --silent http://127.0.0.1:8787/workspace-registry/status +``` + +`/health` is liveness. Registry status verifies branch/head/degraded state and the active validated +snapshot; authenticated `/workspaces` verifies application access. A server with no active snapshot +is not ready for workspace sessions even if liveness succeeds. + +## Pull, publish, upgrade, backup, and recovery + +Use the authenticated Workspace Management UI or `POST /workspace-registry/pull`. Drafts are +browser-local. Publish takes a canonical diff, validates before commit, and pushes under a registry +lock. On `workspace_conflict`, pull, resolve the reviewed field-level draft, validate/test, and +publish; never edit `repo/` inside a running volume. + +For upgrades, record active status/head, drain active Pi work, stop `core`, and take a +filesystem-consistent backup of `/srv/thothii/workspace-registry` plus `/srv/thothii/data`. Exclude +`/srv/thothii/secrets`. Render Compose, deploy the compatible image, verify health/status, then +resume proxy traffic. + +For PSD migration, use a temporary review clone and the legacy transformer with absolute paths. +Its schema-v1 output is `migration_required`; explicitly supply vector database/schema, collection +identity, diagnostics, and the reviewed v2 contract before commit. Never import `${ENV}` values or +copy secret files. + +After valid bootstrap, Git outage retains the active snapshot with `degraded: true`. Repair +egress/DNS/CA/credentials, pull, and confirm healthy status. Roll back a bad descriptor through a +reviewed Git revert/release branch, advance the remote through normal policy, pull it, and confirm +the replacement snapshot. Restore a registry backup only while stopped and with a compatible image; +do not delete snapshots as a rollback shortcut. + +## Troubleshooting and snapshot rollback + +| Stable code | Meaning and safe response | +| --- | --- | +| `workspace_invalid` | Invalid descriptor/path/snapshot; restore a reviewed canonical Git revision. | +| `binding_missing` | Missing/invalid local value or readable `*_FILE`; correct mount and permissions. | +| `workspace_not_activatable` | Bindings/diagnostics cannot activate; use sanitized fields to fix selected transport. | +| `workspace_stale` | Checkout changed/locked; stop concurrent registry work, never force Git in the volume. | +| `workspace_conflict` | Draft base stale; pull, resolve, validate, republish. | +| `git_unavailable` | Storage/remote/DNS/firewall/lock failed; preserve degraded active state while repairing it. | +| `git_auth_failed` | SSH/HTTPS material rejected or unreadable; rotate/fix file without printing it. | +| `git_non_fast_forward` | Checkout diverged; reconcile through registry workflow and branch policy. | +| `git_push_rejected` | Gitea policy rejected publish; review hooks/branch protection. | +| `connector_unavailable` | DNS/TLS/auth/resource identity failed; check egress and local bindings. | +| `semantic_index_incompatible` | Collection/model/dimensions/distance differs; perform explicit index migration. | + +If the current snapshot is valid but Git remains down, continue only work safe on that pinned +revision and monitor status. If snapshots are missing or corrupt, stop the service, restore the +newest verified registry backup, start it privately, verify status, and then reopen proxy traffic. +A first-bootstrap failure has no fallback: repair remote trust rather than creating an unreviewed +runtime checkout. diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh new file mode 100755 index 00000000..49c87c9f --- /dev/null +++ b/scripts/verify-workspace-install-docs.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Validate the installation manuals without reading an operator environment or production remote. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd -P)" +profile="${1:-}" + +case "$profile" in + --profile) + profile="${2:-}" + [[ $# -eq 2 ]] || { echo "usage: $0 --profile {local|server}" >&2; exit 2; } + ;; + *) + echo "usage: $0 --profile {local|server}" >&2 + exit 2 + ;; +esac + +case "$profile" in + local) + manual="$root/docs/install/local-workspace-registry.md" + example="$root/docs/install/examples/local-compose.workspace-registry.yaml" + headings=( + "Prerequisites" + "Git remote: SSH and HTTPS" + "Shared Git values, local bindings, and secret files" + "Direct PostgreSQL, REST, and SSH tunnel bindings" + "Bootstrap, first pull, and diagnostics" + "Publish, update, backup, outage recovery, and rollback" + "Troubleshooting" + ) + ;; + server) + manual="$root/docs/install/server-workspace-registry.md" + example="$root/docs/install/examples/server-compose.workspace-registry.yaml" + headings=( + "Service account, storage, and firewall" + "Gitea and remote Git setup" + "Git credentials, CA, SSH key, and known-hosts mounts" + "Shared Git values, local bindings, and secret files" + "Direct PostgreSQL, REST, and SSH tunnel bindings" + "Same-origin reverse proxy, bootstrap, and health" + "Pull, publish, upgrade, backup, and recovery" + "Troubleshooting and snapshot rollback" + ) + ;; + *) + echo "unknown documentation profile: $profile" >&2 + exit 2 + ;; +esac + +[[ -f "$manual" ]] || { echo "missing $profile installation manual: $manual" >&2; exit 1; } +[[ -f "$example" ]] || { echo "missing $profile Compose example: $example" >&2; exit 1; } + +for heading in "${headings[@]}"; do + grep -Fqx "## $heading" "$manual" >/dev/null || { + echo "missing required heading in $profile manual: $heading" >&2 + exit 1 + } +done + +grep -Fq "$(basename "$example")" "$manual" || { + echo "the $profile manual does not reference its Compose example" >&2 + exit 1 +} + +# Values for secret-bearing variables must be paths. These patterns catch common accidental +# credentials while allowing declarative *_FILE bindings and explicitly empty assignments. +if grep -Ein '(^|[[:space:]])(password|api[_-]?key|token|secret)[[:space:]]*[:=][[:space:]]*[^[:space:]#]' \ + "$manual" "$example" >/dev/null; then + echo "installation documentation contains a secret literal" >&2 + exit 1 +fi + +commands="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs.XXXXXX")" +trap 'rm -f "$commands"' EXIT HUP INT TERM + +# A runnable documentation command is a sh fence immediately following this marker. Commands +# outside the marker are explanatory/operator commands and are deliberately never executed here. +awk ' + /^[[:space:]]*$/ { marked=1; next } + marked && /^```(sh|bash|shell)[[:space:]]*$/ { in_fence=1; marked=0; seen=1; next } + in_fence && /^```[[:space:]]*$/ { in_fence=0; next } + in_fence { print } +' "$manual" >"$commands" + +[[ -s "$commands" ]] || { echo "no marked runnable commands in $profile manual" >&2; exit 1; } + +echo "== Validate $profile documented Compose example ==" +( + cd "$root" + bash "$commands" +) + +echo "== Run isolated workspace-registry bootstrap and recovery smoke ==" +( + cd "$root" + env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh +) + +echo "$profile installation documentation verification passed"