wip: guided standalone installation and workspace checks
This commit is contained in:
@@ -34,11 +34,12 @@ for the executed consolidation and the inventory of historical sources retained
|
|||||||
|
|
||||||
## Docker Compose and installation
|
## Docker Compose and installation
|
||||||
|
|
||||||
For a fresh installation, follow the complete manual procedure in
|
For a fresh installation, follow the guided terminal procedure in
|
||||||
[Italian](docs/install/standalone-manual-it.md) or
|
[Italian](docs/install/standalone-manual-it.md) or
|
||||||
[English](docs/install/standalone-manual-en.md). Configure protected files first;
|
[English](docs/install/standalone-manual-en.md). The single
|
||||||
then run the documented build, explicit migrations and startup commands with the
|
`tht setup --complete` command validates protected files, builds the images, runs
|
||||||
same installation descriptor and Compose project. There is no installer or launcher.
|
Catalog migration, starts the stack and imports the configured workspace repository.
|
||||||
|
There is no graphical installer or native launcher.
|
||||||
|
|
||||||
The mandatory stack includes frontend, core, PostgreSQL catalog, Qdrant, Ollama
|
The mandatory stack includes frontend, core, PostgreSQL catalog, Qdrant, Ollama
|
||||||
and the embedding initializer. DWH and LLM endpoints remain external dependencies.
|
and the embedding initializer. DWH and LLM endpoints remain external dependencies.
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ import { loadSettings } from "./settings/settings-store.js";
|
|||||||
import { ThtRunner, type SessionRow } from "./tht/tht-runner.js";
|
import { ThtRunner, type SessionRow } from "./tht/tht-runner.js";
|
||||||
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
||||||
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
|
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
|
||||||
|
import { createProductionWorkspaceDiagnoser } from "./workspaces/diagnostics.js";
|
||||||
|
import { resolveCatalogRuntimeBinding } from "./catalog/runtime-binding.js";
|
||||||
|
import { CatalogService } from "./catalog/service.js";
|
||||||
|
import { validateOperationalWorkspace } from "./workspaces/schema.js";
|
||||||
import { createCatalogRepository } from "./catalog/repository.js";
|
import { createCatalogRepository } from "./catalog/repository.js";
|
||||||
import type { CatalogRepository } from "./catalog/types.js";
|
import type { CatalogRepository } from "./catalog/types.js";
|
||||||
|
|
||||||
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
|
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
|
||||||
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
|
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
|
||||||
| "pi-test" | "effective-settings";
|
| "workspace-pull" | "workspace-test" | "pi-test" | "effective-settings";
|
||||||
|
|
||||||
const lifecyclePrincipal: PrincipalContext = {
|
const lifecyclePrincipal: PrincipalContext = {
|
||||||
issuer: "tht-operator-command",
|
issuer: "tht-operator-command",
|
||||||
@@ -119,6 +123,119 @@ async function workspaceIntegrity(config: AppConfig): Promise<{
|
|||||||
return { ready: true, ...integrity };
|
return { ready: true, ...integrity };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function workspacePull(config: AppConfig): Promise<{
|
||||||
|
ready: boolean;
|
||||||
|
status: "succeeded" | "degraded";
|
||||||
|
branch: string;
|
||||||
|
head?: string;
|
||||||
|
degraded: boolean;
|
||||||
|
}> {
|
||||||
|
const status = await new WorkspaceRegistry(config.workspaceRegistry).pull();
|
||||||
|
return {
|
||||||
|
ready: !status.degraded,
|
||||||
|
status: status.degraded ? "degraded" : "succeeded",
|
||||||
|
branch: status.branch,
|
||||||
|
...(status.head ? { head: status.head } : {}),
|
||||||
|
degraded: status.degraded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspaceTestReport {
|
||||||
|
id: string;
|
||||||
|
status: "ready" | "failed";
|
||||||
|
database: "reachable" | "not_configured" | "failed";
|
||||||
|
diagnostics: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function workspaceTest(config: AppConfig): Promise<{
|
||||||
|
ready: boolean;
|
||||||
|
workspaces: WorkspaceTestReport[];
|
||||||
|
}> {
|
||||||
|
if (!config.catalogDatabase) throw new Error("Catalog database is not configured");
|
||||||
|
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
||||||
|
const revisions = await registry.list();
|
||||||
|
const repository = createCatalogRepository(config.catalogDatabase);
|
||||||
|
try {
|
||||||
|
const secretStore = new WorkspaceSecretStore({
|
||||||
|
root: config.workspaceSecretStoreRoot,
|
||||||
|
runtimeRoot: config.workspaceSecretRuntimeRoot,
|
||||||
|
installationId: config.workspaceRegistry.installationId,
|
||||||
|
});
|
||||||
|
const catalogService = new CatalogService(
|
||||||
|
repository,
|
||||||
|
registry,
|
||||||
|
secretStore,
|
||||||
|
config.workspaceRegistry.secretRoots,
|
||||||
|
config.workspaceDiagnosticTimeoutMs,
|
||||||
|
);
|
||||||
|
const diagnose = createProductionWorkspaceDiagnoser(config.workspaceDiagnosticTimeoutMs, undefined, {
|
||||||
|
internalQdrantUrl: config.internalQdrantUrl,
|
||||||
|
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
||||||
|
internalEmbeddingId: config.internalEmbeddingId,
|
||||||
|
internalEmbeddingModel: config.internalEmbeddingModel,
|
||||||
|
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
||||||
|
});
|
||||||
|
const databases = await repository.list();
|
||||||
|
const reports: WorkspaceTestReport[] = [];
|
||||||
|
for (const revision of revisions) {
|
||||||
|
const diagnostics: string[] = [];
|
||||||
|
let workspace: ReturnType<typeof validateOperationalWorkspace>;
|
||||||
|
try {
|
||||||
|
workspace = validateOperationalWorkspace((await registry.read(revision.id)).workspace);
|
||||||
|
} catch {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "not_configured", diagnostics: ["workspace_invalid"] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const database = databases.find((candidate) => candidate.workspaceId === revision.id);
|
||||||
|
if (!database) {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "not_configured", diagnostics: ["database_binding_missing"] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tested;
|
||||||
|
try {
|
||||||
|
tested = await catalogService.test(database);
|
||||||
|
} catch {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "failed", diagnostics: ["database_unreachable"] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!tested || tested.connectionStatus !== "reachable") {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "failed", diagnostics: ["database_unreachable"] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lease: ReturnType<typeof resolveCatalogRuntimeBinding>;
|
||||||
|
try {
|
||||||
|
lease = resolveCatalogRuntimeBinding({
|
||||||
|
workspace,
|
||||||
|
database: tested,
|
||||||
|
environment: process.env,
|
||||||
|
secretRoots: config.workspaceRegistry.secretRoots,
|
||||||
|
secretStore,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "reachable", diagnostics: ["binding_missing"] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// CatalogService.test() above is the authoritative database probe and records its
|
||||||
|
// outcome. The remaining diagnoser pass checks Evidence and internal semantic services;
|
||||||
|
// skipping its legacy DWH probe avoids requiring a second response-shape contract for a
|
||||||
|
// REST health endpoint.
|
||||||
|
const result = await diagnose(lease.workspace, lease.bindings, { writeProbe: false, skipDwh: true });
|
||||||
|
diagnostics.push(...result.diagnostics.map((diagnostic) => diagnostic.code));
|
||||||
|
const ready = result.activatable;
|
||||||
|
reports.push({ id: revision.id, status: ready ? "ready" : "failed", database: "reachable", diagnostics });
|
||||||
|
} catch {
|
||||||
|
reports.push({ id: revision.id, status: "failed", database: "reachable", diagnostics: ["connector_unavailable"] });
|
||||||
|
} finally {
|
||||||
|
lease.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ready: reports.length > 0 && reports.every((report) => report.status === "ready"), workspaces: reports };
|
||||||
|
} finally {
|
||||||
|
await repository.close?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function runOperatorAction(
|
export async function runOperatorAction(
|
||||||
action: OperatorAction,
|
action: OperatorAction,
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
@@ -132,6 +249,8 @@ export async function runOperatorAction(
|
|||||||
if (action === "session-inventory") return await sessionInventory(config);
|
if (action === "session-inventory") return await sessionInventory(config);
|
||||||
if (action === "workflow-doctor") return await workflowDiagnostics(config);
|
if (action === "workflow-doctor") return await workflowDiagnostics(config);
|
||||||
if (action === "workspace-integrity") return await workspaceIntegrity(config);
|
if (action === "workspace-integrity") return await workspaceIntegrity(config);
|
||||||
|
if (action === "workspace-pull") return await workspacePull(config);
|
||||||
|
if (action === "workspace-test") return await workspaceTest(config);
|
||||||
const modelCatalog = loadRuntimeModelCatalog(config.modelCatalogFile);
|
const modelCatalog = loadRuntimeModelCatalog(config.modelCatalogFile);
|
||||||
if (action === "effective-settings") {
|
if (action === "effective-settings") {
|
||||||
return effectiveSettings(config, loadSettings(config), modelCatalog);
|
return effectiveSettings(config, loadSettings(config), modelCatalog);
|
||||||
@@ -146,6 +265,7 @@ async function main(): Promise<void> {
|
|||||||
if (!action || ![
|
if (!action || ![
|
||||||
"maintenance-activate", "maintenance-deactivate", "maintenance-status", "session-inventory",
|
"maintenance-activate", "maintenance-deactivate", "maintenance-status", "session-inventory",
|
||||||
"workflow-doctor", "workspace-integrity", "pi-test", "effective-settings",
|
"workflow-doctor", "workspace-integrity", "pi-test", "effective-settings",
|
||||||
|
"workspace-pull", "workspace-test",
|
||||||
].includes(action)) throw new Error("invalid operator action");
|
].includes(action)) throw new Error("invalid operator action");
|
||||||
const result = await runOperatorAction(action, loadConfig(process.env));
|
const result = await runOperatorAction(action, loadConfig(process.env));
|
||||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ vi.mock("../src/tht/tht-runner.js", () => ({
|
|||||||
|
|
||||||
vi.mock("../src/workspaces/registry.js", () => ({
|
vi.mock("../src/workspaces/registry.js", () => ({
|
||||||
WorkspaceRegistry: class {
|
WorkspaceRegistry: class {
|
||||||
|
async pull() {
|
||||||
|
return { branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false };
|
||||||
|
}
|
||||||
|
|
||||||
async listRetainedSnapshots() {
|
async listRetainedSnapshots() {
|
||||||
return [{ snapshotPath: "/data/workspace-registry/snapshots/revision/workspace.yaml" }];
|
return [{ snapshotPath: "/data/workspace-registry/snapshots/revision/workspace.yaml" }];
|
||||||
}
|
}
|
||||||
@@ -98,3 +102,13 @@ test("workflow doctor gives schema-v4 runtime rendering a live Catalog repositor
|
|||||||
expect(fakes.releaseWorkspaceRuntime).toHaveBeenCalledOnce();
|
expect(fakes.releaseWorkspaceRuntime).toHaveBeenCalledOnce();
|
||||||
expect(fakes.catalogRepository.close).toHaveBeenCalledOnce();
|
expect(fakes.catalogRepository.close).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("workspace pull exposes only safe Git status", async () => {
|
||||||
|
await expect(runOperatorAction("workspace-pull", config)).resolves.toEqual({
|
||||||
|
ready: true,
|
||||||
|
status: "succeeded",
|
||||||
|
branch: "main",
|
||||||
|
head: "a".repeat(40),
|
||||||
|
degraded: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ cp deploy/secrets/thothii.secrets.example deploy/secrets/thothii.secrets
|
|||||||
chmod 600 deploy/secrets/thothii.secrets
|
chmod 600 deploy/secrets/thothii.secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a normal local installation, `tht setup --complete` creates the active bundle at
|
||||||
|
`deploy/local/secrets/thothii.secrets` and creates the two Catalog password files beside it. The
|
||||||
|
generated `deploy/local/operator.env` contains only absolute paths to those files; never copy
|
||||||
|
secret values into `operator.env`.
|
||||||
|
|
||||||
The file uses strict `KEY=VALUE` lines (comments and blank lines are allowed). The supported
|
The file uses strict `KEY=VALUE` lines (comments and blank lines are allowed). The supported
|
||||||
installation keys are `THT_MODEL_API_KEY`, `THT_DWH_API_KEY`, `THT_CA`, `THT_SSL_CA`,
|
installation keys are `THT_MODEL_API_KEY`, `THT_DWH_API_KEY`, `THT_CA`, `THT_SSL_CA`,
|
||||||
`THT_OIDC_CLIENT_SECRET`, and `THT_AUTHENTIK_API_TOKEN`. Installation Model Catalog providers may
|
`THT_OIDC_CLIENT_SECRET`, and `THT_AUTHENTIK_API_TOKEN`. Installation Model Catalog providers may
|
||||||
@@ -29,6 +34,11 @@ Session and metadata-generation runtimes read only the provider key named by
|
|||||||
credential reference, not their execution lifecycle. Pi-owned authentication remains available only
|
credential reference, not their execution lifecycle. Pi-owned authentication remains available only
|
||||||
to session-only built-in providers through `authentication.mode: pi_auth`.
|
to session-only built-in providers through `authentication.mode: pi_auth`.
|
||||||
|
|
||||||
|
Workspace database credentials are intentionally not part of this global bundle. Configure each
|
||||||
|
workspace's database binding, password/token, tunnel key and CA in Database Management; the
|
||||||
|
installation stores those values in its encrypted workspace secret store. The workspace Git
|
||||||
|
repository may declare database identity and Evidence, but must never contain these credentials.
|
||||||
|
|
||||||
Do not add vector or embedding endpoint credentials to the bundle. Active operator manuals use
|
Do not add vector or embedding endpoint credentials to the bundle. Active operator manuals use
|
||||||
internal Qdrant and Ollama services, so vector/embedding runtime endpoint secrets are not part of
|
internal Qdrant and Ollama services, so vector/embedding runtime endpoint secrets are not part of
|
||||||
the supported installation contract.
|
the supported installation contract.
|
||||||
|
|||||||
+41
-42
@@ -1,64 +1,63 @@
|
|||||||
# Install and first start
|
# Install and first start
|
||||||
|
|
||||||
Use one complete procedure for a fresh installation:
|
Use the guided procedure for a fresh installation:
|
||||||
|
|
||||||
- [Italian manual installation](standalone-manual-it.md)
|
- [Italian guided installation](standalone-manual-it.md)
|
||||||
- [English manual installation](standalone-manual-en.md)
|
- [English guided installation](standalone-manual-en.md)
|
||||||
|
|
||||||
Both cover macOS, Windows through Ubuntu WSL2, and Linux. They use a Gitea clone,
|
The procedure covers Windows through Ubuntu WSL2, macOS, and Linux including an Omarchy/Arch-like
|
||||||
protected local configuration and manual terminal commands, without an application
|
host. It uses the application clone, one installation secret bundle, protected repository
|
||||||
installer or launcher. See their verification matrix for tests still pending.
|
credentials when needed, and a terminal command. No host Node.js, Python or Pi installation is
|
||||||
|
required.
|
||||||
|
|
||||||
## What must be ready
|
## What must be ready
|
||||||
|
|
||||||
You need Docker with Compose, the host operator command `tht`, access to the workspace
|
You need Docker with Compose v2, Git, Bash, curl, OpenSSL and shasum. You also need access to the
|
||||||
repository, and the credentials and network routes for the configured DWH and model
|
workspace repository and the values supplied by its owner: repository URL/branch, DWH endpoint,
|
||||||
providers. Pi runs inside the application runtime; no host Pi installation is needed.
|
database/schema, transport, credentials or certificates, Evidence credentials when applicable,
|
||||||
|
and LLM provider/API-key information.
|
||||||
|
|
||||||
The stack includes `frontend`, `core`, `catalog-db`, `qdrant`, `embedding`, plus the
|
The workspace repository and the THothII application repository are different. A workspace
|
||||||
one-shot `embedding-model-init` and `catalog-migrate` services. DWH and generative-model
|
descriptor may declare Evidence, but database passwords and installation bindings are stored in
|
||||||
endpoints remain separate installation settings.
|
the installation Catalog, not in Git.
|
||||||
|
|
||||||
Secrets, certificates, Pi authentication and endpoint bindings are protected local files.
|
## One guided command
|
||||||
Do not commit them or copy the configuration of another machine unchanged.
|
|
||||||
|
|
||||||
## Follow the ordered procedure
|
After cloning THothII, checking prerequisites and installing tht, run:
|
||||||
|
|
||||||
The bilingual guides provide the exact commands for:
|
~~~
|
||||||
|
tht setup --complete --profile local --shell-mode full --shell-default-locale en
|
||||||
|
~~~
|
||||||
|
|
||||||
1. Cloning the selected revision and checking prerequisites.
|
The first run creates protected placeholders under deploy/local/secrets/. Fill the required
|
||||||
2. Bootstrapping the native host command.
|
credential files and rerun the same command. The command validates the local files and paths,
|
||||||
3. Preparing catalog passwords and using
|
renders Compose, builds the images, starts catalog-db, runs catalog-migrate, starts the full
|
||||||
`tht setup --profile local --shell-mode full --shell-default-locale en --configure-only`.
|
stack, and pulls/activates the workspace repository. Evidence source files declared by the
|
||||||
4. Completing model, authentication and workspace credentials.
|
workspace are imported during activation.
|
||||||
5. Generating configuration, building images and explicitly running `catalog-migrate`.
|
|
||||||
6. Starting the installation and checking health and readiness.
|
|
||||||
|
|
||||||
Do not run setup alone as a substitute for that sequence. Migrations are not an
|
|
||||||
implicit effect of backend startup or `tht start`. Do not mix this installation's
|
|
||||||
descriptor/project with a different low-level Compose environment.
|
|
||||||
|
|
||||||
For an already configured installation:
|
For an already configured installation:
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
tht --installation /absolute/path/thothii-installation.yaml status
|
tht --installation /absolute/path/thothii-installation.yaml status
|
||||||
tht --installation /absolute/path/thothii-installation.yaml doctor --json
|
tht --installation /absolute/path/thothii-installation.yaml doctor --json
|
||||||
```
|
tht --installation /absolute/path/thothii-installation.yaml workspace test --json
|
||||||
|
~~~
|
||||||
|
|
||||||
`/health` checks application-process readiness. Doctor also checks configuration,
|
doctor --json is the non-destructive general core test. workspace test also probes the configured
|
||||||
workspace, workflow and Pi prerequisites; a healthy web page alone does not prove
|
database, Evidence, Qdrant and embedding service for every active workspace. It requires the
|
||||||
that a real database question can complete.
|
workspace database to have been configured in Database Management first.
|
||||||
|
|
||||||
## After startup
|
## Installer-only completion
|
||||||
|
|
||||||
Prepare [workspaces](../operations/workspaces.md), configure a database in
|
The installer must still decide which LLMs and API keys are approved, configure and test each
|
||||||
[Database Management](../operations/database-management.md), and complete the functional
|
workspace database, synchronize its schema, generate and consolidate descriptions, create Qdrant
|
||||||
checks in the installation guide before using real data.
|
entries, review naming-based FK suggestions alongside schema FKs, and load the approved
|
||||||
|
relationships. The final declaration of completeness requires green doctor and workspace test
|
||||||
|
results plus one real natural-language question completed through final SQL.
|
||||||
|
|
||||||
See [display mode and language](shell-and-language.md), [local authentication](authentication-local.md),
|
Migrations are part of setup --complete. Do not mix this installation’s descriptor or volumes with
|
||||||
[OIDC](authentication-oidc.md) and [model configuration](../general/pi-configuration.md)
|
a different Compose environment. Preserve the descriptor, credentials, Catalog and persistent
|
||||||
for later changes. Embedded portal integration is separate from a fresh standalone setup.
|
volumes; do not use docker compose down --volumes as a routine stop.
|
||||||
|
|
||||||
Use the installation's normal `tht start`, `tht stop` and diagnostic commands.
|
See the guides for the Windows/macOS/Linux prerequisite matrix, workspace repository explanation,
|
||||||
Preserve its descriptor, credentials, database and persistent volumes; do not use
|
secret layout and Gate A/Gate B acceptance checks.
|
||||||
`down --volumes` as a routine stop or upgrade.
|
|
||||||
|
|||||||
@@ -1,324 +1,256 @@
|
|||||||
# Manual standalone installation
|
# Guided standalone installation
|
||||||
|
|
||||||
[Versione italiana](standalone-manual-it.md)
|
[Versione italiana](standalone-manual-it.md)
|
||||||
|
|
||||||
This is the verification procedure for preparing THothII as a standalone application in `full`
|
This is the fresh-machine installation procedure for THothII. THothII receives a natural-language
|
||||||
mode on macOS, Windows, and Linux.
|
question, queries an enterprise database read-only, and guides the user through SQL review. The
|
||||||
|
core, PostgreSQL catalog, Qdrant, embedding service, and Pi run in Docker; Node.js, Python, and Pi
|
||||||
|
are not required on the host.
|
||||||
|
|
||||||
In this document, “standalone” means that the user does not need to install Node.js, Python or Pi
|
## Before you start: the two repositories
|
||||||
on the host: the application services and local semantic
|
|
||||||
services run through Docker. DWH and LLM providers remain external endpoints configured by the
|
|
||||||
installation; this is not an offline package.
|
|
||||||
|
|
||||||
This path uses no graphical installer, native launcher, or Docker Hub image. It starts from a Gitea
|
There are two separate repositories:
|
||||||
clone and uses explicit terminal commands. Publishing pre-built images is a later step.
|
|
||||||
|
|
||||||
## Verification matrix
|
1. the application repository cloned by the user:
|
||||||
|
https://git.tylconsulting.it/mptyl/ThothII.git;
|
||||||
|
2. the workspace repository supplied by the curator/installer. It is not the THothII repository
|
||||||
|
and must not be cloned inside the application directory.
|
||||||
|
|
||||||
| System | Recommended terminal | Runtime | Test architecture |
|
The workspace repository normally contains:
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| macOS supported by the installed Docker Desktop version | Bash in Terminal | Docker Desktop | Apple Silicon (`arm64`) |
|
|
||||||
| Windows 11 | Ubuntu inside WSL2 | Docker Desktop with WSL2 integration | x64 (`amd64`) |
|
|
||||||
| Ubuntu Linux 22.04 or 24.04 | Bash | Docker Engine + Compose v2 | x64 (`amd64`) |
|
|
||||||
|
|
||||||
Intel macOS is excluded from the first verification campaign. ARM Linux may be tested when the
|
~~~
|
||||||
machine’s Docker runtime reports `arm64`, but it is not part of the minimum matrix.
|
thoth-workspaces.yaml
|
||||||
|
<workspace-id>/workspace.yaml
|
||||||
|
<workspace-id>/evidence/** # when Evidence is declared
|
||||||
|
~~~
|
||||||
|
|
||||||
Documentation and Mac prerequisite checks have passed. Complete fresh installations on all three
|
workspace.yaml contains workspace identity, language and optional Evidence source. By design it does
|
||||||
systems remain pending; this matrix describes the tests to perform, not completed certification.
|
not contain database passwords. Database identity, transport (PostgreSQL, REST, or tunnel), user,
|
||||||
|
password, token and certificates are installation-local settings stored encrypted by the Catalog.
|
||||||
|
This prevents credentials from being committed to the workspace repository.
|
||||||
|
|
||||||
## Before you start
|
## 0. Machine prerequisites
|
||||||
|
|
||||||
You need:
|
### Windows
|
||||||
|
|
||||||
- access to the THothII Gitea repository and the workspace Git repository;
|
- Windows 10/11 with Docker Desktop running and the WSL2 backend enabled.
|
||||||
- Git;
|
- Ubuntu in WSL2, with Docker Desktop integration enabled for that distribution.
|
||||||
- Docker Desktop on macOS and Windows, or Docker Engine with the Compose v2 plugin on Linux;
|
- Git, Bash, curl, OpenSSL and shasum inside WSL2.
|
||||||
- Bash, `curl`, OpenSSL and `shasum` (Ubuntu package: `libdigest-sha-perl`);
|
- Do not install Node.js, Python or Pi on the host for this procedure.
|
||||||
- enough disk space to build the images and download the embedding model;
|
|
||||||
- the DWH and LLM endpoints, plus the credentials required by the installation.
|
|
||||||
|
|
||||||
On Linux, the current user must be able to run Docker. If the system requires `sudo`, add the user
|
If WSL2 is not installed, use the company procedure or, in PowerShell:
|
||||||
to the Docker group according to local policy and open a new session before continuing.
|
|
||||||
|
|
||||||
On Windows, run every command in this guide from Ubuntu under WSL2. In Docker Desktop, enable WSL2
|
~~~
|
||||||
integration for that distribution. Clone the project inside the WSL2 Linux filesystem, for example
|
wsl --install -d Ubuntu
|
||||||
under `~/src`, rather than under `/mnt/c`: this avoids slow builds and path/line-ending issues. Pi
|
~~~
|
||||||
does not need to be installed on the host.
|
|
||||||
|
|
||||||
Check the runtime before or immediately after cloning:
|
Run all commands inside Ubuntu WSL2, in a Linux directory such as $HOME/src, not under /mnt/c.
|
||||||
|
scripts/install-tht.ps1 exists for advanced native PowerShell scenarios; use WSL2 for the
|
||||||
|
reproducible test.
|
||||||
|
|
||||||
```sh
|
### macOS
|
||||||
docker version
|
|
||||||
|
- Docker Desktop installed and running, with several GB free for images and the embedding model.
|
||||||
|
- Git, Bash, curl, OpenSSL and shasum.
|
||||||
|
- Intel and Apple Silicon Macs are supported when Docker Desktop supports the architecture
|
||||||
|
reported by the Docker server.
|
||||||
|
- Do not install Node.js, Python or Pi on the host for this procedure.
|
||||||
|
|
||||||
|
### Linux, including Omarchy
|
||||||
|
|
||||||
|
- Git, Bash, curl, OpenSSL and shasum.
|
||||||
|
- Docker Engine and the Docker Compose v2 plugin. On Omarchy, check first:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
command -v docker
|
||||||
docker compose version
|
docker compose version
|
||||||
|
docker info
|
||||||
|
~~~
|
||||||
|
|
||||||
|
If Docker is missing, install Docker and Compose using the distribution-approved package procedure,
|
||||||
|
then start the service. On an Arch-like distribution the typical route is:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
sudo pacman -S docker docker-compose
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
After adding the group, open a new session and repeat docker info. Node.js, Python and Pi are not
|
||||||
|
needed on the host: they are in the Docker images.
|
||||||
|
|
||||||
|
On every system run:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
docker version --format '{{.Server.Arch}}'
|
docker version --format '{{.Server.Arch}}'
|
||||||
```
|
~~~
|
||||||
|
|
||||||
The last command must return `amd64`, `x86_64`, `arm64`, or `aarch64`.
|
The architecture must be amd64, x86_64, arm64, or aarch64. You also need access to the
|
||||||
|
application Gitea repository, the workspace repository URL/branch and credentials, container
|
||||||
|
reachability to DWH/LLM endpoints, and the credentials, tokens or certificates associated with
|
||||||
|
the databases.
|
||||||
|
|
||||||
## 1. Clone a project revision
|
## 1. What to clone
|
||||||
|
|
||||||
Use the project repository on Gitea:
|
Clone only the application:
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
mkdir -p "$HOME/src"
|
mkdir -p "$HOME/src"
|
||||||
cd "$HOME/src"
|
cd "$HOME/src"
|
||||||
git clone https://git.tylconsulting.it/mptyl/ThothII.git
|
git clone https://git.tylconsulting.it/mptyl/ThothII.git
|
||||||
cd ThothII
|
cd ThothII
|
||||||
git rev-parse --short HEAD
|
git rev-parse --short HEAD
|
||||||
```
|
~~~
|
||||||
|
|
||||||
For an SSH clone, when the key is already authorized on Gitea:
|
Record the revision. tht setup --complete downloads the workspace repository into a persistent
|
||||||
|
Docker volume using the URL, branch and transport supplied during setup.
|
||||||
|
|
||||||
```sh
|
## 2. Install the terminal command
|
||||||
git clone git@git.tylconsulting.it:mptyl/ThothII.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Record the hash printed by `git rev-parse` for a repeatable test. In a later campaign, use the
|
|
||||||
maintainer-approved revision/tag rather than implicitly following a mutable `main` branch.
|
|
||||||
|
|
||||||
## 2. Check prerequisites and install the operator command
|
|
||||||
|
|
||||||
From the clone root:
|
From the clone root:
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
bash scripts/check-standalone-prerequisites.sh
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
mkdir -p "$HOME/.local/bin"
|
||||||
THT_INSTALL_DIRECTORY="$HOME/.local/bin" bash scripts/install-tht.sh
|
THT_INSTALL_DIRECTORY="$HOME/.local/bin" bash scripts/install-tht.sh
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
tht version
|
tht version
|
||||||
```
|
~~~
|
||||||
|
|
||||||
`install-tht.sh` bootstraps only the native `tht` operator command; it does not install a desktop
|
tht is the only native component to install. It builds the binary with Docker and orchestrates
|
||||||
version of THothII. It uses the repository’s Docker builder, installs the binary for the current
|
Compose; it is not a second application runtime.
|
||||||
terminal environment, and installs it in the user directory. Persist `$HOME/.local/bin` in your
|
|
||||||
shell PATH for new terminals too. An existing `tht` in this directory will be updated.
|
|
||||||
|
|
||||||
On Windows, run these commands inside WSL2. The installed `tht` binary is the Linux binary inside
|
## 3. Prepare a few secrets and run complete setup
|
||||||
WSL2; the application runtime remains Docker Desktop. Do not use `scripts/install-tht.ps1` as the
|
|
||||||
primary path for this test.
|
|
||||||
|
|
||||||
## 3. Configure and start the local installation
|
The first execution creates protected placeholders under deploy/local/secrets/ and stops if a
|
||||||
|
required credential is missing. Fill in the requested files and rerun the same command; compatible
|
||||||
|
configuration files are reused.
|
||||||
|
|
||||||
Run the remaining blocks in one Bash session from the physical clone root (`pwd -P`).
|
~~~
|
||||||
First create two distinct catalog passwords, preserving any existing files:
|
tht setup --complete --profile local --shell-mode full --shell-default-locale en
|
||||||
|
~~~
|
||||||
|
|
||||||
```bash
|
The setup asks only for information the computer cannot know:
|
||||||
umask 077
|
|
||||||
mkdir -p deploy/local/secrets
|
|
||||||
for name in catalog-runtime-password catalog-migrator-password; do
|
|
||||||
target="deploy/local/secrets/$name"
|
|
||||||
if [ ! -e "$target" ]; then
|
|
||||||
(set -C; openssl rand -hex 32 > "$target") || exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
export THT_CATALOG_RUNTIME_PASSWORD_SOURCE="$(pwd -P)/deploy/local/secrets/catalog-runtime-password"
|
|
||||||
export THT_CATALOG_MIGRATOR_PASSWORD_SOURCE="$(pwd -P)/deploy/local/secrets/catalog-migrator-password"
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not regenerate passwords for an initialized catalog. Configure without starting services:
|
| Request | What to provide |
|
||||||
|
|
||||||
```sh
|
|
||||||
tht setup --profile local --shell-mode full --shell-default-locale en --configure-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Answer the prompts as follows:
|
|
||||||
|
|
||||||
| Prompt | Value or rule |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Installation ID | `local`, unless one clone hosts multiple installations |
|
| Workspace repository | Data/configuration repository URL, not ThothII.git |
|
||||||
| Deployment profile | `local` |
|
| Branch | normally main |
|
||||||
| DWH API endpoint | An `http(s)` URL without user, password, query, or fragment; may be empty for a smoke-only test |
|
| Access | ssh with key and known_hosts, or https with credential file and CA |
|
||||||
| LLM API endpoint | An `http(s)` URL without credentials; may be empty for a smoke-only test |
|
| DWH/LLM URL | endpoint without a token in the URL |
|
||||||
| Workspace repository URL | The workspace repository URL, not the THothII source clone |
|
| Local login | initial user and password requested by the prompt |
|
||||||
| Workspace branch | Normally `main` |
|
|
||||||
| Workspace access | `ssh` with a deploy key, or `https` with a protected credential file |
|
|
||||||
| File paths | Accept the default paths under `deploy/local/secrets/` for the first test |
|
|
||||||
| Secret templates | Answer `yes` when protected files do not exist yet |
|
|
||||||
| Authentication | Configure the local login required by the installation; never put passwords on a command line |
|
|
||||||
|
|
||||||
The generated configuration is local and ignored by Git:
|
The setup generates random Catalog passwords and writes their paths, never their values, to
|
||||||
|
operator.env. It runs docker compose config, builds images, starts the Catalog, runs
|
||||||
|
catalog-migrate, starts the stack, and pulls the workspace repository. The pull also activates
|
||||||
|
declared Evidence; at minimum source files present in the workspace are materialized locally.
|
||||||
|
|
||||||
```text
|
### The file the user fills in
|
||||||
deploy/local/thothii-installation.yaml
|
|
||||||
deploy/local/operator.env
|
|
||||||
deploy/local/auth/
|
|
||||||
deploy/local/secrets/
|
|
||||||
```
|
|
||||||
|
|
||||||
Edit secrets only in protected local files; never commit them. `deploy/env/local.env.example` is a tracked reference; the
|
The main file is:
|
||||||
generated path `deploy/local/operator.env` is the active path for this installation.
|
|
||||||
|
|
||||||
### Complete protected files
|
~~~
|
||||||
|
deploy/local/secrets/thothii.secrets
|
||||||
|
~~~
|
||||||
|
|
||||||
If setup created blank templates, enter the values with a local editor:
|
Add only NAME=VALUE lines needed by modelCatalog and installation adapters, such as an LLM API key
|
||||||
|
(DEEPSEEK_API_KEY, OPENAI_API_KEY, or the key declared by the catalog) and, when applicable,
|
||||||
|
THT_DWH_API_KEY. Allowed names are documented in deploy/secrets/README.md. Never put tokens in
|
||||||
|
URLs, the repository, or copied shell commands.
|
||||||
|
|
||||||
```sh
|
Two distinctions prevent common errors:
|
||||||
chmod 600 deploy/local/secrets/*
|
|
||||||
"${EDITOR:-vi}" deploy/local/secrets/thothii.secrets
|
|
||||||
```
|
|
||||||
|
|
||||||
The bundle must contain only `KEY=VALUE` lines for credentials actually used by `modelCatalog`. The
|
- when the catalog uses pi_auth, the LLM token belongs in the Pi pi-auth.json file created by
|
||||||
allowed names and credential boundary are documented in the local file
|
setup; {} is only a placeholder and does not enable a model;
|
||||||
`deploy/secrets/README.md`. Do not put tokens in URLs, the YAML
|
- workspace database credentials (PostgreSQL password, REST API token, tunnel SSH key,
|
||||||
descriptor, the Git repository, or commands copied into the shell.
|
known_hosts, CA) do not belong in the workspace repository. Enter them per workspace in
|
||||||
|
Database Management, which stores them encrypted in the Catalog. The workspace declares
|
||||||
|
database/schema and transport; the installer must obtain the actual values from the database owner.
|
||||||
|
|
||||||
For SSH workspace access, also provide the private key and `known_hosts` file requested by setup.
|
A private workspace repository also needs the Git files required by its transport: an SSH key and
|
||||||
For HTTPS access, provide the Git credential file and any required CA. Both must remain protected
|
known_hosts, or an HTTPS credential file and CA. These are transport files, not a second bundle to
|
||||||
and outside version control.
|
commit. To minimize manual files, use SSH with an already-authorized deploy key.
|
||||||
|
|
||||||
Before starting, complete these additional configuration steps:
|
## 4. Automatic checks and terminal tests
|
||||||
|
|
||||||
1. Add `THT_CATALOG_RUNTIME_PASSWORD_SOURCE` and `THT_CATALOG_MIGRATOR_PASSWORD_SOURCE` to
|
Setup verifies files, permissions, descriptor, Compose, Docker, authentication, services, Pi and
|
||||||
`deploy/local/operator.env`, with the same absolute paths exported above. Setup does not persist
|
the workspace. After startup, run these commands at any time:
|
||||||
these two variables. Store paths, not passwords.
|
|
||||||
2. Replace the descriptor's generic `modelCatalog` with the approved provider/model configuration.
|
|
||||||
The generated defaults do not replicate the existing Mac. See [Pi/model configuration](../general/pi-configuration.md)
|
|
||||||
and the local example `deploy/psd/thothii-installation.yaml.example`.
|
|
||||||
3. Populate the keys referenced by `authentication.apiKeyEnv` in `thothii.secrets`. Providers using
|
|
||||||
`pi_auth` need valid credentials at `PI_AUTH_FILE`; the `{}` template is not authentication.
|
|
||||||
4. Complete the workspace Git files. SSH requires an authorized deploy key and verified known-hosts;
|
|
||||||
HTTPS requires the credential file and CA bundle expected by the overlay. Blank templates cannot
|
|
||||||
provide repository access.
|
|
||||||
|
|
||||||
After editing generated configuration, do not rerun setup: it rejects different existing content.
|
~~~
|
||||||
Generate the projections and run the explicit migration below. Use `THT_GIT_ACCESS=https` if that
|
INSTALLATION="$PWD/deploy/local/thothii-installation.yaml"
|
||||||
was selected during setup. This block targets the fresh `local` descriptor with only the Git overlay;
|
tht --installation "$INSTALLATION" doctor --json
|
||||||
custom installations must include their extra descriptor overlays in the same order.
|
tht --installation "$INSTALLATION" workspace pull --json
|
||||||
|
tht --installation "$INSTALLATION" workspace test --json
|
||||||
|
~~~
|
||||||
|
|
||||||
```bash
|
workspace test checks, for every active workspace, database binding and credentials, Evidence,
|
||||||
INSTALLATION="$(pwd -P)/deploy/local/thothii-installation.yaml"
|
Qdrant, and the embedding service. It exits non-zero when the database binding is missing or a
|
||||||
tht --installation "$INSTALLATION" installation generate
|
connection is unusable. Before running it, the installer must configure the database in Database
|
||||||
THT_PROJECT="thothii-$(printf '%s' "$INSTALLATION" | shasum -a 256 | cut -c 1-12)"
|
Management: the workspace repository cannot contain the password by itself.
|
||||||
THT_GIT_ACCESS=ssh
|
|
||||||
compose=(
|
|
||||||
docker compose --project-name "$THT_PROJECT" --project-directory "$(pwd -P)"
|
|
||||||
--env-file "$(pwd -P)/deploy/local/operator.env"
|
|
||||||
-f compose.yaml -f deploy/compose.local.yaml
|
|
||||||
-f "deploy/compose.git-$THT_GIT_ACCESS.yaml"
|
|
||||||
-f deploy/local/generated/compose.models.yaml
|
|
||||||
)
|
|
||||||
"${compose[@]}" config --quiet
|
|
||||||
"${compose[@]}" build core frontend
|
|
||||||
"${compose[@]}" up -d catalog-db
|
|
||||||
"${compose[@]}" run --rm catalog-migrate
|
|
||||||
tht --installation "$INSTALLATION" start
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop if a command fails. The project name matches the hash used by `tht`, preserving volume
|
doctor --json is the repeatable, non-destructive core verification. The final functional test must
|
||||||
identity. `catalog-migrate` applies Catalog and Memory migrations; `tht start` does not run it
|
also open http://127.0.0.1:8080, sign in, and complete a real question through final SQL.
|
||||||
automatically. Initial embedding-model download may take time. Use this installation-specific
|
|
||||||
sequence, not `run-stack.sh` with a different environment/project name.
|
|
||||||
|
|
||||||
## 4. Verify the installation
|
## Activities only the installer can complete
|
||||||
|
|
||||||
The descriptor generated for the default ID is:
|
The procedure automates bootstrap, but it cannot invent enterprise decisions or authorizations.
|
||||||
|
The installer must complete and record:
|
||||||
|
|
||||||
```sh
|
1. usable LLMs, the modelCatalog, and linked API keys; then run tht pi test and tht doctor;
|
||||||
INSTALLATION="$(pwd -P)/deploy/local/thothii-installation.yaml"
|
2. database configuration, connection test, schema synchronization, and description generation;
|
||||||
test -f "$INSTALLATION"
|
3. human consolidation of generated descriptions;
|
||||||
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
4. Qdrant semantic entries through workspace preprocess run;
|
||||||
```
|
5. naming-based FK suggestions as a complement to schema FKs, human review, and loading approved
|
||||||
|
relationships into Qdrant;
|
||||||
|
6. recurring tht doctor --json and tht workspace test --json checks;
|
||||||
|
7. one real question completed successfully without connection or model errors.
|
||||||
|
|
||||||
The verifier is read-only: it runs `tht doctor --json` and `tht status` without restarting the
|
Configuration is complete only when all applicable activities are done, decisions are recorded, and
|
||||||
stack, regenerating configuration, or printing secret contents.
|
the two terminal tests are green. The core is usable only after the real question, not merely
|
||||||
|
because the frontend answers /health.
|
||||||
|
|
||||||
### Gate A — platform smoke test on all three computers
|
## Gate A and Gate B
|
||||||
|
|
||||||
Record the following for each machine:
|
### Gate A — platform
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
uname -a
|
uname -a
|
||||||
docker version --format '{{.Server.Version}} {{.Server.Arch}}'
|
docker version --format '{{.Server.Version}} {{.Server.Arch}}'
|
||||||
tht version
|
tht version
|
||||||
bash scripts/check-standalone-prerequisites.sh
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
||||||
```
|
~~~
|
||||||
|
|
||||||
The gate passes when the clone is intact, Docker and Compose are reachable, `tht doctor` is OK, the
|
### Gate B — usability
|
||||||
stack is running, and the frontend responds at the default local URL `http://127.0.0.1:8080`.
|
|
||||||
Doctor also checks workspace and Pi: record their failures separately rather than labeling every
|
|
||||||
failure as a platform problem. Check HTTP readiness with:
|
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
curl --fail --silent --show-error http://127.0.0.1:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate B — functional verification
|
|
||||||
|
|
||||||
Run this on at least one machine with available endpoints and credentials:
|
|
||||||
|
|
||||||
First follow [Workspace operations](../operations/workspaces.md) to import/prepare the workspace
|
|
||||||
and configure the Database and local binding. The source clone does not transfer catalog data,
|
|
||||||
secrets or sessions from the Mac. Connect any VPN required by DWH/model endpoints and verify
|
|
||||||
that their names are reachable from containers too.
|
|
||||||
|
|
||||||
1. open `http://127.0.0.1:8080`;
|
|
||||||
2. sign in with the configured local account;
|
|
||||||
3. verify that the configured workspace is readable;
|
|
||||||
4. start a real question and complete the review gates through final SQL;
|
|
||||||
5. stop and restart the installation, then run `verify-standalone-install.sh` again.
|
|
||||||
|
|
||||||
A Gate B failure involving DWH, the LLM provider, workspace Git, or credentials does not by itself
|
|
||||||
prove a Docker portability problem: record the failed endpoint or component separately.
|
|
||||||
|
|
||||||
## Daily lifecycle
|
|
||||||
|
|
||||||
Use the explicit descriptor when more than one installation may be discoverable:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
INSTALLATION="$PWD/deploy/local/thothii-installation.yaml"
|
|
||||||
|
|
||||||
tht --installation "$INSTALLATION" status
|
|
||||||
tht --installation "$INSTALLATION" start
|
|
||||||
tht --installation "$INSTALLATION" start --build
|
|
||||||
tht --installation "$INSTALLATION" logs
|
|
||||||
tht --installation "$INSTALLATION" doctor --json
|
tht --installation "$INSTALLATION" doctor --json
|
||||||
tht --installation "$INSTALLATION" stop
|
tht --installation "$INSTALLATION" workspace test --json
|
||||||
```
|
curl --fail --silent --show-error http://127.0.0.1:8080/health
|
||||||
|
~~~
|
||||||
|
|
||||||
Use `start --build` after source changes or to rebuild images from the current checkout. `stop`
|
Then run a real question and stop/restart with tht stop and tht start. Do not use docker compose
|
||||||
preserves volumes, sessions, the catalog, Pi state, Qdrant data, and the embedding model. Do not
|
down --volumes: it deletes the Catalog, sessions, Qdrant data and the embedding model.
|
||||||
use `docker compose down --volumes` during a normal test: it is destructive and removes local data.
|
|
||||||
For upgrades requiring migrations, follow the release runbook before starting the new application.
|
|
||||||
|
|
||||||
## Quick diagnosis
|
## Quick diagnosis
|
||||||
|
|
||||||
| Symptom | Check |
|
| Symptom | Action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `Docker Engine is not reachable` | start Docker Desktop or the Docker service and rerun `docker info` |
|
| Docker Engine is not reachable | start Docker Desktop or systemctl and repeat docker info |
|
||||||
| Windows sees Docker but Bash fails | run the guide inside Ubuntu WSL2 and enable that distribution in Docker Desktop |
|
| Omarchy cannot find docker | install Docker/Compose, enable the service and open a new session |
|
||||||
| `tht: command not found` | open a new shell and check `command -v tht`; rerun the bootstrap if needed |
|
| Windows sees Docker but Bash fails | use Ubuntu WSL2 and enable its Docker Desktop integration |
|
||||||
| line-ending or executable-script errors | use a clone in the WSL2/Linux filesystem and rerun `bash scripts/...` |
|
| workspace pull fails | check URL, branch, key/credential file and known_hosts from the container |
|
||||||
| unsupported architecture | check `docker version --format '{{.Server.Arch}}'`; the test requires `amd64` or `arm64` |
|
| workspace test reports a missing binding | configure database, token/password and CA in Database Management |
|
||||||
| missing descriptor or env file | use `deploy/local/...` generated by `tht setup`, not an arbitrary copied file |
|
| Pi is not ready | fill pi-auth.json or the key declared by modelCatalog, then run tht pi test |
|
||||||
| healthy stack but workflow failure | check external URLs, the credential bundle, workspace Git, and authentication separately |
|
|
||||||
| data appears missing | check that `down --volumes` was not used; `stop` does not remove volumes |
|
|
||||||
|
|
||||||
## Acceptance checklist
|
|
||||||
|
|
||||||
- [ ] The clone comes from the expected Gitea repository and the revision is recorded.
|
|
||||||
- [ ] Docker Desktop/Engine and Compose v2 are available.
|
|
||||||
- [ ] The runtime reports an allowed architecture.
|
|
||||||
- [ ] `tht` was built from the repository and responds to `tht version`.
|
|
||||||
- [ ] Setup uses `profile: local`, `shell.mode: full`, and `shell.defaultLocale: en`.
|
|
||||||
- [ ] The descriptor, `operator.env`, authentication, and secrets exist only under `deploy/local/`.
|
|
||||||
- [ ] No secret appears in Git, URLs, public YAML, or recorded commands.
|
|
||||||
- [ ] Gate A passes on Apple Silicon macOS, x64 Windows WSL2, and x64 Linux.
|
|
||||||
- [ ] Gate B runs on at least one machine with DWH and LLM available.
|
|
||||||
- [ ] Stop/start and final verification complete without deleting volumes.
|
|
||||||
|
|
||||||
## Out of scope for this release
|
|
||||||
|
|
||||||
The following remain future work:
|
|
||||||
|
|
||||||
- publishing pre-built images on Docker Hub;
|
|
||||||
- reducing prompts through a dedicated non-interactive configuration;
|
|
||||||
- creating DMG, MSI/EXE, AppImage, or other native installers;
|
|
||||||
- providing an offline runtime or bundling a local DWH/LLM into the application.
|
|
||||||
|
|
||||||
## Related documents
|
## Related documents
|
||||||
|
|
||||||
- [Install and first start](first-start.md)
|
- [Install and first start](first-start.md)
|
||||||
- [Shell and localization](shell-and-language.md)
|
|
||||||
- [Workspace operations](../operations/workspaces.md)
|
- [Workspace operations](../operations/workspaces.md)
|
||||||
- `deploy/secrets/README.md` (runtime secrets)
|
- [Database Management](../operations/database-management.md)
|
||||||
|
- [Model configuration](../general/pi-configuration.md)
|
||||||
|
- deploy/secrets/README.md
|
||||||
|
|
||||||
|
Publishing images on Docker Hub and native DMG/MSI/AppImage installers remain later work: this
|
||||||
|
procedure starts from the Gitea clone and does not require pre-published Docker Hub images.
|
||||||
|
|||||||
@@ -1,329 +1,262 @@
|
|||||||
# Installazione manuale standalone
|
# Installazione standalone guidata
|
||||||
|
|
||||||
[English version](standalone-manual-en.md)
|
[English version](standalone-manual-en.md)
|
||||||
|
|
||||||
Questa è la procedura di prova per predisporre THothII come applicazione autonoma in modalità
|
Questa è la procedura per installare THothII da zero. THothII riceve una domanda in linguaggio
|
||||||
`full` su macOS, Windows e Linux.
|
naturale, interroga in sola lettura un database aziendale e accompagna l’utente nella revisione
|
||||||
|
della SQL risultante. Il core, il catalogo PostgreSQL, Qdrant, il servizio di embedding e Pi vengono
|
||||||
|
eseguiti in Docker; sul computer non servono Node.js, Python o Pi.
|
||||||
|
|
||||||
In questo documento “autonoma” significa che l’utente non deve installare Node.js, Python o Pi
|
## Prima di iniziare: i due repository
|
||||||
sull'host: i servizi applicativi e i servizi semantici
|
|
||||||
locali vengono eseguiti con Docker. DWH e provider LLM restano endpoint esterni configurati
|
|
||||||
dall’installazione; questa procedura non è un pacchetto offline.
|
|
||||||
|
|
||||||
Il percorso non usa installer grafici, launcher nativi o immagini Docker Hub. Si parte da un clone
|
Servono due repository distinti:
|
||||||
Gitea e si usano comandi espliciti da terminale. La pubblicazione di immagini pre-costruite è una
|
|
||||||
fase successiva.
|
|
||||||
|
|
||||||
## Matrice di verifica
|
1. il repository dell’applicazione, che l’utente clona:
|
||||||
|
https://git.tylconsulting.it/mptyl/ThothII.git;
|
||||||
|
2. il repository dei workspace, indicato dal curatore/installatore. Non è il repository di
|
||||||
|
THothII e non va clonato manualmente nella directory dell’applicazione.
|
||||||
|
|
||||||
| Sistema | Terminale raccomandato | Runtime | Architettura della prova |
|
Il repository workspace contiene il catalogo e una directory per ogni workspace, normalmente:
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| macOS supportato dalla versione Docker Desktop installata | Bash nel Terminale | Docker Desktop | Apple Silicon (`arm64`) |
|
|
||||||
| Windows 11 | Ubuntu dentro WSL2 | Docker Desktop con integrazione WSL2 | x64 (`amd64`) |
|
|
||||||
| Linux Ubuntu 22.04 o 24.04 | Bash | Docker Engine + Compose v2 | x64 (`amd64`) |
|
|
||||||
|
|
||||||
Intel macOS non fa parte della prima campagna di verifica. ARM Linux può essere provato quando il
|
~~~
|
||||||
runtime Docker della macchina restituisce `arm64`, ma non è un requisito della matrice minima.
|
thoth-workspaces.yaml
|
||||||
|
<workspace-id>/workspace.yaml
|
||||||
|
<workspace-id>/evidence/** # se il workspace dichiara Evidence
|
||||||
|
~~~
|
||||||
|
|
||||||
Le verifiche documentali e dei prerequisiti Mac sono passate. Le installazioni complete da zero
|
Il file workspace.yaml descrive identità, lingua e, opzionalmente, la sorgente Evidence. Per scelta
|
||||||
sui tre sistemi restano da eseguire: la matrice indica le prove previste, non una certificazione.
|
architetturale non contiene password del database. L’identità del database, il trasporto
|
||||||
|
(PostgreSQL, REST o tunnel), username, password, token e certificati sono configurazione locale
|
||||||
|
dell’installazione, conservata cifrata dal Catalog. Questo evita di committare credenziali nel
|
||||||
|
repository workspace.
|
||||||
|
|
||||||
## Cosa serve prima di iniziare
|
## 0. Prerequisiti della macchina
|
||||||
|
|
||||||
Servono:
|
### Windows
|
||||||
|
|
||||||
- accesso al repository Gitea di THothII e al repository Git dei workspace;
|
- Windows 10/11 con Docker Desktop avviato e backend WSL2 abilitato.
|
||||||
- Git;
|
- Ubuntu in WSL2, con integrazione Docker Desktop abilitata per quella distribuzione.
|
||||||
- Docker Desktop su macOS e Windows, oppure Docker Engine con il plugin Compose v2 su Linux;
|
- Git, Bash, curl, OpenSSL e shasum nella distribuzione WSL2.
|
||||||
- Bash, `curl`, OpenSSL e `shasum` (su Ubuntu, pacchetto `libdigest-sha-perl`);
|
- Non installare Node.js, Python o Pi sull’host per questa procedura.
|
||||||
- spazio disco sufficiente per compilare le immagini e scaricare il modello di embedding;
|
|
||||||
- gli endpoint DWH e LLM, più le credenziali che l’installazione deve usare.
|
|
||||||
|
|
||||||
Su Linux l’utente corrente deve poter eseguire Docker. Se il sistema richiede `sudo`, aggiungere
|
In PowerShell, se WSL2 non esiste ancora, usare la procedura aziendale oppure:
|
||||||
l’utente al gruppo Docker secondo la policy locale e aprire una nuova sessione prima di continuare.
|
|
||||||
|
|
||||||
Su Windows usare Ubuntu in WSL2 per tutti i comandi di questa guida. In Docker Desktop attivare
|
~~~
|
||||||
l’integrazione WSL2 per quella distribuzione. Clonare il progetto nel filesystem Linux di WSL2,
|
wsl --install -d Ubuntu
|
||||||
per esempio sotto `~/src`, e non sotto `/mnt/c`: si evitano rallentamenti e problemi di permessi o
|
~~~
|
||||||
line ending. Non è necessario installare Pi sull’host.
|
|
||||||
|
|
||||||
Verificare il runtime prima del clone o subito dopo:
|
Eseguire poi tutti i comandi dentro Ubuntu WSL2, in una directory Linux come $HOME/src, non sotto
|
||||||
|
/mnt/c. Il percorso nativo scripts/install-tht.ps1 esiste per scenari PowerShell avanzati; per la
|
||||||
|
prova riproducibile usare WSL2.
|
||||||
|
|
||||||
```sh
|
### macOS
|
||||||
docker version
|
|
||||||
|
- Docker Desktop installato, avviato e con alcuni GB liberi per immagini e modello di embedding.
|
||||||
|
- Git, Bash, curl, OpenSSL e shasum.
|
||||||
|
- Sono supportati Mac Intel e Apple Silicon se Docker Desktop supporta l’architettura restituita
|
||||||
|
dal Docker server.
|
||||||
|
- Non installare Node.js, Python o Pi sull’host per questa procedura.
|
||||||
|
|
||||||
|
### Linux, incluso Omarchy
|
||||||
|
|
||||||
|
- Git, Bash, curl, OpenSSL e shasum.
|
||||||
|
- Docker Engine e il plugin Docker Compose v2. Su Omarchy verificare prima:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
command -v docker
|
||||||
docker compose version
|
docker compose version
|
||||||
|
docker info
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Se Docker manca, installare Docker e Compose con il gestore pacchetti/procedura approvata dalla
|
||||||
|
distribuzione, poi avviare il servizio. Su una distribuzione Arch-like il percorso tipico è:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
sudo pacman -S docker docker-compose
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Dopo l’aggiunta al gruppo aprire una nuova sessione e ripetere docker info. Non installare Node.js,
|
||||||
|
Python o Pi sull’host: sono dentro le immagini Docker.
|
||||||
|
|
||||||
|
Su tutti i sistemi il controllo finale è:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
docker version --format '{{.Server.Arch}}'
|
docker version --format '{{.Server.Arch}}'
|
||||||
```
|
~~~
|
||||||
|
|
||||||
L’ultima istruzione deve restituire `amd64`, `x86_64`, `arm64` o `aarch64`.
|
L’architettura deve essere amd64, x86_64, arm64 o aarch64. Servono inoltre accesso al repository
|
||||||
|
Gitea dell’applicazione, URL/branch e credenziali del repository workspace, raggiungibilità dal
|
||||||
|
container degli endpoint DWH/LLM e le credenziali, token o certificati associati ai database.
|
||||||
|
|
||||||
## 1. Clonare una revisione del progetto
|
## 1. Cosa clonare
|
||||||
|
|
||||||
Usare il repository di progetto su Gitea:
|
Clonare solo l’applicazione:
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
mkdir -p "$HOME/src"
|
mkdir -p "$HOME/src"
|
||||||
cd "$HOME/src"
|
cd "$HOME/src"
|
||||||
git clone https://git.tylconsulting.it/mptyl/ThothII.git
|
git clone https://git.tylconsulting.it/mptyl/ThothII.git
|
||||||
cd ThothII
|
cd ThothII
|
||||||
git rev-parse --short HEAD
|
git rev-parse --short HEAD
|
||||||
```
|
~~~
|
||||||
|
|
||||||
Per un clone SSH usare, se la chiave è già autorizzata su Gitea:
|
Annotare la revisione. Il repository workspace verrà scaricato da tht setup --complete dentro un
|
||||||
|
volume Docker persistente, usando URL, branch e trasporto indicati durante il setup.
|
||||||
|
|
||||||
```sh
|
## 2. Installare il comando terminale
|
||||||
git clone git@git.tylconsulting.it:mptyl/ThothII.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Per una prova ripetibile annotare l’hash stampato da `git rev-parse`. In una campagna successiva
|
|
||||||
usare la revisione/tag approvata dal maintainer invece di seguire implicitamente una `main` che
|
|
||||||
può cambiare.
|
|
||||||
|
|
||||||
## 2. Verificare i prerequisiti e installare il comando operatore
|
|
||||||
|
|
||||||
Dal root del clone:
|
Dal root del clone:
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
bash scripts/check-standalone-prerequisites.sh
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
mkdir -p "$HOME/.local/bin"
|
||||||
THT_INSTALL_DIRECTORY="$HOME/.local/bin" bash scripts/install-tht.sh
|
THT_INSTALL_DIRECTORY="$HOME/.local/bin" bash scripts/install-tht.sh
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
tht version
|
tht version
|
||||||
```
|
~~~
|
||||||
|
|
||||||
`install-tht.sh` è un bootstrap del solo comando operatore nativo `tht`; non installa una versione
|
Il comando tht è l’unico componente nativo da installare. Costruisce il binario con Docker e
|
||||||
desktop di THothII. Usa il builder Docker del repository, installa il binario adatto all’ambiente
|
orchestra Compose; non è un secondo runtime dell’applicazione.
|
||||||
del terminale e lo installa nella directory utente. Aggiungere `$HOME/.local/bin` al PATH della
|
|
||||||
shell anche per i terminali successivi. Un `tht` già presente in quella directory viene aggiornato.
|
|
||||||
|
|
||||||
Su Windows, eseguire questi comandi dentro WSL2. Il binario `tht` installato è quello Linux di WSL2;
|
## 3. Preparare pochi segreti e avviare il setup completo
|
||||||
il runtime dell’applicazione rimane Docker Desktop. Non usare `scripts/install-tht.ps1` come percorso
|
|
||||||
principale di questa prova.
|
|
||||||
|
|
||||||
## 3. Configurare e avviare l’installazione locale
|
La prima esecuzione crea i placeholder protetti sotto deploy/local/secrets/ e si ferma se manca
|
||||||
|
una credenziale necessaria. Compilare i file indicati e rilanciare lo stesso comando: i file di
|
||||||
|
configurazione già compatibili vengono riutilizzati.
|
||||||
|
|
||||||
Eseguire i blocchi successivi nella stessa sessione Bash dal root fisico del clone (`pwd -P`).
|
~~~
|
||||||
Creare prima le due password distinte del catalogo, conservando eventuali file già esistenti:
|
tht setup --complete --profile local --shell-mode full --shell-default-locale en
|
||||||
|
~~~
|
||||||
|
|
||||||
```bash
|
Durante il setup servono solo le informazioni operative che il computer non può conoscere:
|
||||||
umask 077
|
|
||||||
mkdir -p deploy/local/secrets
|
|
||||||
for name in catalog-runtime-password catalog-migrator-password; do
|
|
||||||
target="deploy/local/secrets/$name"
|
|
||||||
if [ ! -e "$target" ]; then
|
|
||||||
(set -C; openssl rand -hex 32 > "$target") || exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
export THT_CATALOG_RUNTIME_PASSWORD_SOURCE="$(pwd -P)/deploy/local/secrets/catalog-runtime-password"
|
|
||||||
export THT_CATALOG_MIGRATOR_PASSWORD_SOURCE="$(pwd -P)/deploy/local/secrets/catalog-migrator-password"
|
|
||||||
```
|
|
||||||
|
|
||||||
Non rigenerare le password di un catalogo già inizializzato. Configurare senza avviare i servizi:
|
| Richiesta | Cosa inserire |
|
||||||
|
|
||||||
```sh
|
|
||||||
tht setup --profile local --shell-mode full --shell-default-locale en --configure-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Rispondere ai prompt nel seguente modo:
|
|
||||||
|
|
||||||
| Prompt | Valore o regola |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Installation ID | `local`, salvo necessità di più installazioni nello stesso clone |
|
| Repository workspace | URL del repository dati/configurazione, non ThothII.git |
|
||||||
| Deployment profile | `local` |
|
| Branch | normalmente main |
|
||||||
| DWH API endpoint | URL `http(s)` senza user, password, query o fragment; può restare vuoto per il solo smoke test |
|
| Accesso | ssh con chiave e known_hosts, oppure https con credential file e CA |
|
||||||
| LLM API endpoint | URL `http(s)` senza credenziali; può restare vuoto per il solo smoke test |
|
| DWH/LLM URL | endpoint senza token nella URL |
|
||||||
| Workspace repository URL | URL del repository dei workspace, non il clone sorgente di THothII |
|
| Login locale | utente e password iniziale richiesti dal prompt |
|
||||||
| Workspace branch | normalmente `main` |
|
|
||||||
| Workspace access | `ssh` se si usa una chiave deploy; altrimenti `https` con credential file protetto |
|
|
||||||
| Percorsi dei file | accettare i percorsi predefiniti sotto `deploy/local/secrets/` nella prima prova |
|
|
||||||
| Secret templates | rispondere `yes` quando i file protetti non esistono ancora |
|
|
||||||
| Autenticazione | configurare il login locale richiesto dall’installazione; non inserire password in una riga di comando |
|
|
||||||
|
|
||||||
La configurazione generata è locale e ignorata da Git:
|
Il setup crea automaticamente le password casuali del Catalog e le scrive in operator.env come
|
||||||
|
percorsi, non come valori. Esegue docker compose config, costruisce le immagini, avvia il Catalog,
|
||||||
|
esegue catalog-migrate, avvia lo stack e importa il repository workspace. L’import attiva anche
|
||||||
|
l’Evidence dichiarata: almeno i file source presenti nel workspace vengono materializzati nel
|
||||||
|
registro locale.
|
||||||
|
|
||||||
```text
|
### Il file da compilare
|
||||||
deploy/local/thothii-installation.yaml
|
|
||||||
deploy/local/operator.env
|
|
||||||
deploy/local/auth/
|
|
||||||
deploy/local/secrets/
|
|
||||||
```
|
|
||||||
|
|
||||||
Modificare i segreti solo nei file locali protetti; non committarli. Il file `deploy/env/local.env.example` è un riferimento
|
Il file principale è:
|
||||||
tracciato; il percorso generato da `tht setup`, `deploy/local/operator.env`, è quello da usare per
|
|
||||||
questa installazione.
|
|
||||||
|
|
||||||
### Completare i file protetti
|
~~~
|
||||||
|
deploy/local/secrets/thothii.secrets
|
||||||
|
~~~
|
||||||
|
|
||||||
Se il setup ha creato template vuoti, inserire i valori con un editor locale:
|
Inserire solo righe NOME=VALORE necessarie al modelCatalog e agli adapter, per esempio una API key
|
||||||
|
LLM (DEEPSEEK_API_KEY, OPENAI_API_KEY o quella dichiarata dal catalogo) ed eventualmente
|
||||||
|
THT_DWH_API_KEY. I nomi ammessi sono documentati in deploy/secrets/README.md. Non mettere token
|
||||||
|
nelle URL, nel repository o nei comandi.
|
||||||
|
|
||||||
```sh
|
Due precisazioni evitano gli errori più comuni:
|
||||||
chmod 600 deploy/local/secrets/*
|
|
||||||
"${EDITOR:-vi}" deploy/local/secrets/thothii.secrets
|
|
||||||
```
|
|
||||||
|
|
||||||
Il bundle deve contenere solo righe `KEY=VALUE` per le credenziali effettivamente usate dal
|
- se il catalogo usa pi_auth, il token LLM va nel file Pi pi-auth.json creato dal setup; {} è solo
|
||||||
`modelCatalog`. I nomi ammessi e il confine delle credenziali sono descritti nel file locale
|
un placeholder e non abilita alcun modello;
|
||||||
`deploy/secrets/README.md`. Non mettere token nelle URL, nel
|
- le credenziali specifiche di un database workspace (password PostgreSQL, API token REST, chiave
|
||||||
descriptor YAML, nel repository Git o nei comandi copiati nella shell.
|
SSH del tunnel, known_hosts, CA) non vanno nel repository workspace: si inseriscono per workspace
|
||||||
|
in Database Management, che le conserva nel Catalog cifrato. Il workspace indica database/schema
|
||||||
|
e trasporto; l’installatore deve ottenere dal proprietario il valore corretto.
|
||||||
|
|
||||||
Per accesso workspace SSH, predisporre anche la chiave privata e il file `known_hosts` indicati dal
|
Per un repository workspace privato sono inoltre indispensabili i file Git richiesti dal trasporto:
|
||||||
setup. Per accesso HTTPS, predisporre il credential file Git e l’eventuale CA. Entrambi devono
|
una chiave SSH e known_hosts, oppure credential file HTTPS e CA. Sono file di trasporto, non un
|
||||||
restare protetti e fuori dal controllo versione.
|
secondo bundle da committare. Per ridurre i file da compilare, usare SSH con una deploy key già
|
||||||
|
autorizzata.
|
||||||
|
|
||||||
Prima dell'avvio completare anche questi passaggi:
|
## 4. Controlli automatici e test da terminale
|
||||||
|
|
||||||
1. Aggiungere `THT_CATALOG_RUNTIME_PASSWORD_SOURCE` e `THT_CATALOG_MIGRATOR_PASSWORD_SOURCE` a
|
Il setup verifica file, permessi, descriptor, Compose, Docker, autenticazione, servizi, Pi e
|
||||||
`deploy/local/operator.env`, con gli stessi percorsi assoluti esportati sopra. Il setup non salva
|
workspace. Dopo l’avvio usare questi comandi in qualunque momento:
|
||||||
queste due variabili. Inserire i percorsi, non le password.
|
|
||||||
2. Sostituire il `modelCatalog` generico nel descriptor con la configurazione provider/modelli
|
|
||||||
approvata. I default generati non replicano il Mac esistente. Vedere [configurazione Pi/modelli](../general/pi-configuration.md)
|
|
||||||
e l'esempio locale `deploy/psd/thothii-installation.yaml.example`.
|
|
||||||
3. Inserire in `thothii.secrets` le chiavi referenziate da `authentication.apiKeyEnv`. I provider
|
|
||||||
`pi_auth` richiedono credenziali valide nel file `PI_AUTH_FILE`; il template `{}` non autentica.
|
|
||||||
4. Completare i file Git del workspace. SSH richiede una chiave deploy autorizzata e known-hosts
|
|
||||||
verificato; HTTPS richiede il credential file e il bundle CA previsti dall'overlay. I template
|
|
||||||
vuoti non consentono l'accesso al repository.
|
|
||||||
|
|
||||||
Dopo aver modificato la configurazione generata, non rilanciare setup: rifiuta file esistenti con
|
~~~
|
||||||
contenuti diversi. Generare le proiezioni ed eseguire la migrazione esplicita indicata sotto.
|
INSTALLATION="$PWD/deploy/local/thothii-installation.yaml"
|
||||||
Impostare `THT_GIT_ACCESS=https` se scelto nel setup. Il blocco usa il nuovo descriptor `local`
|
tht --installation "$INSTALLATION" doctor --json
|
||||||
con il solo overlay Git; per installazioni personalizzate includere gli overlay aggiuntivi
|
tht --installation "$INSTALLATION" workspace pull --json
|
||||||
nello stesso ordine del descriptor.
|
tht --installation "$INSTALLATION" workspace test --json
|
||||||
|
~~~
|
||||||
|
|
||||||
```bash
|
workspace test prova, per ogni workspace attivo, il binding del database e le credenziali, Evidence,
|
||||||
INSTALLATION="$(pwd -P)/deploy/local/thothii-installation.yaml"
|
Qdrant e il servizio embedding. Restituisce exit code diverso da zero se manca il binding del
|
||||||
tht --installation "$INSTALLATION" installation generate
|
database o una connessione non è utilizzabile. Prima di eseguirlo l’installatore deve aver
|
||||||
THT_PROJECT="thothii-$(printf '%s' "$INSTALLATION" | shasum -a 256 | cut -c 1-12)"
|
configurato il database in Database Management: il workspace repository da solo non può contenere
|
||||||
THT_GIT_ACCESS=ssh
|
la password.
|
||||||
compose=(
|
|
||||||
docker compose --project-name "$THT_PROJECT" --project-directory "$(pwd -P)"
|
|
||||||
--env-file "$(pwd -P)/deploy/local/operator.env"
|
|
||||||
-f compose.yaml -f deploy/compose.local.yaml
|
|
||||||
-f "deploy/compose.git-$THT_GIT_ACCESS.yaml"
|
|
||||||
-f deploy/local/generated/compose.models.yaml
|
|
||||||
)
|
|
||||||
"${compose[@]}" config --quiet
|
|
||||||
"${compose[@]}" build core frontend
|
|
||||||
"${compose[@]}" up -d catalog-db
|
|
||||||
"${compose[@]}" run --rm catalog-migrate
|
|
||||||
tht --installation "$INSTALLATION" start
|
|
||||||
```
|
|
||||||
|
|
||||||
Fermarsi se un comando fallisce. Il nome progetto coincide con l'hash usato da `tht`, preservando
|
Per una verifica generale del core, doctor --json è il test ripetibile e non distruttivo. Il test
|
||||||
l'identità dei volumi. `catalog-migrate` applica le migrazioni Catalog e Memory; `tht start` non
|
funzionale finale deve inoltre aprire http://127.0.0.1:8080, autenticarsi e completare una domanda
|
||||||
lo esegue automaticamente. Il primo download del modello embedding può richiedere tempo.
|
reale fino alla SQL finale.
|
||||||
Usare questa sequenza legata all'installazione, non `run-stack.sh` con env/progetto diversi.
|
|
||||||
|
|
||||||
## 4. Verificare l’installazione
|
## Attività che può svolgere solo l’installatore
|
||||||
|
|
||||||
Il descriptor generato per l’ID predefinito è:
|
La procedura automatizza il bootstrap, non può inventare decisioni o autorizzazioni aziendali.
|
||||||
|
L’installatore deve completare e registrare:
|
||||||
|
|
||||||
```sh
|
1. quali LLM sono utilizzabili, il relativo modelCatalog e le API key collegate; poi eseguire
|
||||||
INSTALLATION="$(pwd -P)/deploy/local/thothii-installation.yaml"
|
tht pi test e tht doctor;
|
||||||
test -f "$INSTALLATION"
|
2. per ogni database: configurazione, test connessione, sincronizzazione dello schema e
|
||||||
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
generazione delle descrizioni;
|
||||||
```
|
3. consolidamento umano delle descrizioni generate;
|
||||||
|
4. generazione delle entry semantiche in Qdrant tramite workspace preprocess run;
|
||||||
|
5. generazione delle FK suggerite dal naming, come complemento alle FK lette dallo schema, revisione
|
||||||
|
umana delle proposte e caricamento delle relazioni approvate in Qdrant;
|
||||||
|
6. verifica periodica con tht doctor --json e tht workspace test --json;
|
||||||
|
7. una domanda reale completata con successo, senza errori di connessione o modello.
|
||||||
|
|
||||||
Il verificatore è read-only: esegue `tht doctor --json` e `tht status`, senza ristartare lo stack,
|
La configurazione è dichiarata completa solo quando tutti i punti applicabili sono stati eseguiti,
|
||||||
rigenerare la configurazione o stampare il contenuto dei segreti.
|
le decisioni sono state registrate e i due test terminali sono verdi. Il core è dichiarato usabile
|
||||||
|
solo dopo la domanda reale, non perché il frontend risponde a /health.
|
||||||
|
|
||||||
### Gate A — smoke di piattaforma, su tutti e tre i computer
|
## Gate A e Gate B
|
||||||
|
|
||||||
Registrare per ogni macchina:
|
### Gate A — piattaforma
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
uname -a
|
uname -a
|
||||||
docker version --format '{{.Server.Version}} {{.Server.Arch}}'
|
docker version --format '{{.Server.Version}} {{.Server.Arch}}'
|
||||||
tht version
|
tht version
|
||||||
bash scripts/check-standalone-prerequisites.sh
|
bash scripts/check-standalone-prerequisites.sh
|
||||||
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
bash scripts/verify-standalone-install.sh "$INSTALLATION"
|
||||||
```
|
~~~
|
||||||
|
|
||||||
Il gate passa quando il clone è integro, Docker e Compose sono raggiungibili, `tht doctor` è OK,
|
### Gate B — usabilità
|
||||||
lo stack è avviato e il frontend risponde sulla porta locale predefinita `http://127.0.0.1:8080`.
|
|
||||||
Il doctor controlla anche workspace e Pi: registrare separatamente i loro errori senza attribuire
|
|
||||||
ogni fallimento alla piattaforma. Verificare la disponibilità HTTP con:
|
|
||||||
|
|
||||||
```sh
|
~~~
|
||||||
curl --fail --silent --show-error http://127.0.0.1:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate B — verifica funzionale
|
|
||||||
|
|
||||||
Eseguire almeno su una macchina con endpoint e credenziali disponibili:
|
|
||||||
|
|
||||||
Seguire prima [Workspace operations](../operations/workspaces.md) per importare/preparare il
|
|
||||||
workspace e configurare Database e binding locale. Il clone sorgente non trasferisce catalogo,
|
|
||||||
segreti o sessioni del Mac. Connettere l'eventuale VPN richiesta da DWH/modelli e verificare
|
|
||||||
che i relativi nomi siano raggiungibili anche dai container.
|
|
||||||
|
|
||||||
1. aprire `http://127.0.0.1:8080`;
|
|
||||||
2. autenticarsi con l’account locale configurato;
|
|
||||||
3. verificare che il workspace configurato sia leggibile;
|
|
||||||
4. avviare una domanda reale e completare i gate di revisione fino alla SQL finale;
|
|
||||||
5. fermare e riavviare l’installazione, poi ripetere `verify-standalone-install.sh`.
|
|
||||||
|
|
||||||
Un fallimento del Gate B per DWH, provider LLM, Git workspace o credenziali non dimostra da solo un
|
|
||||||
problema di portabilità Docker: registrare separatamente l’endpoint o il componente fallito.
|
|
||||||
|
|
||||||
## Ciclo di vita quotidiano
|
|
||||||
|
|
||||||
Usare il descriptor esplicito quando più installazioni possono essere scoperte:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
INSTALLATION="$PWD/deploy/local/thothii-installation.yaml"
|
|
||||||
|
|
||||||
tht --installation "$INSTALLATION" status
|
|
||||||
tht --installation "$INSTALLATION" start
|
|
||||||
tht --installation "$INSTALLATION" start --build
|
|
||||||
tht --installation "$INSTALLATION" logs
|
|
||||||
tht --installation "$INSTALLATION" doctor --json
|
tht --installation "$INSTALLATION" doctor --json
|
||||||
tht --installation "$INSTALLATION" stop
|
tht --installation "$INSTALLATION" workspace test --json
|
||||||
```
|
curl --fail --silent --show-error http://127.0.0.1:8080/health
|
||||||
|
~~~
|
||||||
|
|
||||||
`start --build` è necessario dopo una modifica al codice o per ricostruire le immagini dal clone
|
Poi eseguire una domanda reale e fermare/riavviare con tht stop e tht start. Non usare docker
|
||||||
corrente. `stop` conserva volumi, sessioni, catalogo, stato Pi, dati Qdrant e modello embedding.
|
compose down --volumes: cancella Catalog, sessioni, Qdrant e il modello embedding.
|
||||||
Non usare `docker compose down --volumes` durante una prova normale: è un’operazione distruttiva
|
|
||||||
che cancella i dati locali.
|
|
||||||
Per aggiornamenti che richiedono migrazioni, seguire il runbook della release prima dell'avvio.
|
|
||||||
|
|
||||||
## Diagnosi rapida
|
## Diagnosi rapida
|
||||||
|
|
||||||
| Sintomo | Controllo |
|
| Sintomo | Azione |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `Docker Engine is not reachable` | avviare Docker Desktop oppure il servizio Docker e ripetere `docker info` |
|
| Docker Engine is not reachable | avviare Docker Desktop o systemctl e ripetere docker info |
|
||||||
| Windows vede Docker ma Bash fallisce | eseguire la guida dentro Ubuntu WSL2 e abilitare l’integrazione della distribuzione in Docker Desktop |
|
| Omarchy non trova docker | installare Docker/Compose, abilitare il servizio e riaprire la sessione |
|
||||||
| `tht: command not found` | aprire una nuova shell e verificare `command -v tht`; se necessario ripetere il bootstrap |
|
| Windows vede Docker ma Bash fallisce | usare Ubuntu WSL2 e abilitarne l’integrazione in Docker Desktop |
|
||||||
| line ending o script non eseguibile | usare un clone nel filesystem WSL2/Linux e rieseguire `bash scripts/...` |
|
| pull workspace fallisce | controllare URL, branch, chiave/credential file e known_hosts dal container |
|
||||||
| architettura non supportata | verificare `docker version --format '{{.Server.Arch}}'`; la prova richiede `amd64` o `arm64` |
|
| workspace test segnala binding mancante | configurare database, token/password e CA in Database Management |
|
||||||
| descriptor o env file mancanti | usare il percorso `deploy/local/...` generato da `tht setup`, non un file copiato casualmente |
|
| Pi non è pronto | compilare pi-auth.json o la chiave dichiarata dal modelCatalog, poi eseguire tht pi test |
|
||||||
| stack sano ma workflow fallisce | controllare separatamente URL, credential bundle, workspace Git e autenticazione |
|
|
||||||
| dati apparentemente persi | verificare che non sia stato usato `down --volumes`; `stop` non rimuove i volumi |
|
|
||||||
|
|
||||||
## Checklist di accettazione
|
|
||||||
|
|
||||||
- [ ] Il clone proviene dal repository Gitea atteso e la revisione è stata annotata.
|
|
||||||
- [ ] Docker Desktop/Engine e Compose v2 sono disponibili.
|
|
||||||
- [ ] Il runtime restituisce un’architettura ammessa.
|
|
||||||
- [ ] `tht` è stato costruito dal repository e risponde a `tht version`.
|
|
||||||
- [ ] Il setup usa `profile: local`, `shell.mode: full` e `shell.defaultLocale: en`.
|
|
||||||
- [ ] Descriptor, `operator.env`, autenticazione e segreti sono presenti solo in `deploy/local/`.
|
|
||||||
- [ ] Nessun segreto compare in Git, URL, YAML pubblico o comandi registrati.
|
|
||||||
- [ ] Gate A superato su macOS Apple Silicon, Windows WSL2/x64 e Linux x64.
|
|
||||||
- [ ] Gate B eseguito almeno su una macchina con DWH e LLM disponibili.
|
|
||||||
- [ ] Stop/start e verifica finale completati senza cancellare i volumi.
|
|
||||||
|
|
||||||
## Fuori perimetro di questa release
|
|
||||||
|
|
||||||
Restano attività successive:
|
|
||||||
|
|
||||||
- pubblicare immagini pre-costruite su Docker Hub;
|
|
||||||
- ridurre ulteriormente i prompt tramite una configurazione non interattiva dedicata;
|
|
||||||
- creare pacchetti DMG, MSI/EXE, AppImage o altri installer nativi;
|
|
||||||
- fornire un runtime offline o un DWH/LLM locale incluso nell’applicazione.
|
|
||||||
|
|
||||||
## Documenti collegati
|
## Documenti collegati
|
||||||
|
|
||||||
- [Install and first start](first-start.md)
|
- [Installazione e primo avvio](first-start.md)
|
||||||
- [Shell and localization](shell-and-language.md)
|
- [Operazioni sui workspace](../operations/workspaces.md)
|
||||||
- [Workspace operations](../operations/workspaces.md)
|
- [Database Management](../operations/database-management.md)
|
||||||
- `deploy/secrets/README.md` (runtime secrets)
|
- [Configurazione dei modelli](../general/pi-configuration.md)
|
||||||
|
- deploy/secrets/README.md
|
||||||
|
|
||||||
|
La pubblicazione di immagini su Docker Hub e gli installer nativi DMG/MSI/AppImage restano attività
|
||||||
|
successive: questa procedura parte dal clone Gitea e non richiede immagini Docker Hub pre-pubblicate.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Omics. Le preferenze non modificano il descrittore installato.
|
|||||||
Per una nuova installazione autonoma, selezionare esplicitamente full:
|
Per una nuova installazione autonoma, selezionare esplicitamente full:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tht setup --profile local --shell-mode full --shell-default-locale en
|
tht setup --complete --profile local --shell-mode full --shell-default-locale en
|
||||||
```
|
```
|
||||||
|
|
||||||
Il setup senza opzioni shell conserva per compatibilità il default embedded.
|
Il setup senza opzioni shell conserva per compatibilità il default embedded.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ verify_standalone_installation_guides() {
|
|||||||
'git clone https://git.tylconsulting.it/mptyl/ThothII.git' \
|
'git clone https://git.tylconsulting.it/mptyl/ThothII.git' \
|
||||||
'scripts/check-standalone-prerequisites.sh' \
|
'scripts/check-standalone-prerequisites.sh' \
|
||||||
'scripts/install-tht.sh' \
|
'scripts/install-tht.sh' \
|
||||||
'tht setup --profile local --shell-mode full --shell-default-locale en' \
|
'tht setup --complete --profile local --shell-mode full --shell-default-locale en' \
|
||||||
'scripts/verify-standalone-install.sh' \
|
'scripts/verify-standalone-install.sh' \
|
||||||
'Docker Hub' \
|
'Docker Hub' \
|
||||||
'Gate A' \
|
'Gate A' \
|
||||||
@@ -99,8 +99,7 @@ verify_install_and_workspace_guides() {
|
|||||||
require_file "$workspace"
|
require_file "$workspace"
|
||||||
|
|
||||||
for text in \
|
for text in \
|
||||||
'tht setup --profile local' \
|
'tht setup --complete --profile local' \
|
||||||
'--configure-only' \
|
|
||||||
'catalog-migrate' \
|
'catalog-migrate' \
|
||||||
'tht --installation /absolute/path/thothii-installation.yaml doctor --json'; do
|
'tht --installation /absolute/path/thothii-installation.yaml doctor --json'; do
|
||||||
require_text "$install" "$text"
|
require_text "$install" "$text"
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ When --installation is omitted, tht uses THOTHII_INSTALLATION or discovers one v
|
|||||||
descriptor in the current project tree.
|
descriptor in the current project tree.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
setup [--configure-only] [--installation-id ID] [--profile local|server]
|
setup [--complete|--configure-only] [--installation-id ID] [--profile local|server]
|
||||||
[--shell-mode full|embedded] [--shell-default-locale BCP47-TAG] [--shell-adapter omics-portal]
|
[--shell-mode full|embedded] [--shell-default-locale BCP47-TAG] [--shell-adapter omics-portal]
|
||||||
Create or validate the local non-secret installation configuration.
|
Create, validate, and optionally complete the local installation.
|
||||||
installation migrate --output PATH --session-default PROVIDER/MODEL
|
installation migrate --output PATH --session-default PROVIDER/MODEL
|
||||||
--embedding-id PROVIDER/MODEL --embedding-dimensions N
|
--embedding-id PROVIDER/MODEL --embedding-dimensions N
|
||||||
Create a review-only schema-v2 candidate from all three legacy model sources.
|
Create a review-only schema-v2 candidate from all three legacy model sources.
|
||||||
@@ -86,6 +86,10 @@ Commands:
|
|||||||
Verify a terminal installation, remove stale lifecycle files, and clear maintenance.
|
Verify a terminal installation, remove stale lifecycle files, and clear maintenance.
|
||||||
pi logs Show the latest 200 sanitized core log lines (bounded; no follow mode).
|
pi logs Show the latest 200 sanitized core log lines (bounded; no follow mode).
|
||||||
workspace inspect --workspace ID [--json]
|
workspace inspect --workspace ID [--json]
|
||||||
|
workspace pull [--json]
|
||||||
|
Pull and activate the configured workspace repository.
|
||||||
|
workspace test [--json]
|
||||||
|
Test configured database, Evidence, Qdrant, and embedding connectivity.
|
||||||
workspace evidence consolidate --workspace ID [--json]
|
workspace evidence consolidate --workspace ID [--json]
|
||||||
workspace evidence refresh --workspace ID [--json]
|
workspace evidence refresh --workspace ID [--json]
|
||||||
workspace evidence decide --workspace ID --source-id SHA --revision SHA --decision keep|replace [--json]
|
workspace evidence decide --workspace ID --source-id SHA --revision SHA --decision keep|replace [--json]
|
||||||
@@ -400,6 +404,11 @@ func parseSetupArgs(args []string) (setup.Request, error) {
|
|||||||
flag := args[0]
|
flag := args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
switch flag {
|
switch flag {
|
||||||
|
case "--complete":
|
||||||
|
if request.Complete {
|
||||||
|
return setup.Request{}, errors.New("--complete may be supplied once")
|
||||||
|
}
|
||||||
|
request.Complete = true
|
||||||
case "--configure-only":
|
case "--configure-only":
|
||||||
if request.ConfigureOnly {
|
if request.ConfigureOnly {
|
||||||
return setup.Request{}, errors.New("--configure-only may be supplied once")
|
return setup.Request{}, errors.New("--configure-only may be supplied once")
|
||||||
@@ -484,6 +493,9 @@ func parseSetupArgs(args []string) (setup.Request, error) {
|
|||||||
*target = value
|
*target = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if request.Complete && request.ConfigureOnly {
|
||||||
|
return setup.Request{}, errors.New("--complete and --configure-only cannot be combined")
|
||||||
|
}
|
||||||
return request, nil
|
return request, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,6 +511,9 @@ func writeRemovalTargets(outputWriter io.Writer, project string, targets []serve
|
|||||||
}
|
}
|
||||||
|
|
||||||
func workspaceCommand(ctx context.Context, installation config.Installation, runner compose.Runner, args []string, secretValues []string, stdout, stderr io.Writer) int {
|
func workspaceCommand(ctx context.Context, installation config.Installation, runner compose.Runner, args []string, secretValues []string, stdout, stderr io.Writer) int {
|
||||||
|
if len(args) > 0 && (args[0] == "pull" || args[0] == "test") {
|
||||||
|
return workspaceOperatorCommand(ctx, installation, runner, args, secretValues, stdout, stderr)
|
||||||
|
}
|
||||||
request, err := workspaceops.Parse(args)
|
request, err := workspaceops.Parse(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandUsageError(stderr, err.Error())
|
return commandUsageError(stderr, err.Error())
|
||||||
@@ -527,6 +542,51 @@ func workspaceCommand(ctx context.Context, installation config.Installation, run
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func workspaceOperatorCommand(ctx context.Context, installation config.Installation, runner compose.Runner, args []string, secretValues []string, stdout, stderr io.Writer) int {
|
||||||
|
action := "workspace-" + args[0]
|
||||||
|
jsonMode := false
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
if arg != "--json" || jsonMode {
|
||||||
|
return commandUsageError(stderr, "workspace pull/test accepts only --json")
|
||||||
|
}
|
||||||
|
jsonMode = true
|
||||||
|
}
|
||||||
|
result, err := runner.Run(ctx, installation.ComposeArgs("exec", "-T", "core", "node", "dist/operator-command.js", action), nil)
|
||||||
|
if err != nil {
|
||||||
|
return writeResult(result, err, secretValues, stdout, stderr)
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(result.Stdout), &payload); err != nil {
|
||||||
|
fmt.Fprintln(stderr, "tht: workspace operator returned invalid JSON")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if jsonMode {
|
||||||
|
fmt.Fprintln(stdout, output.Sanitize(result.Stdout, secretValues))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(stdout, "workspace %s: %s\n", args[0], output.Sanitize(workspaceOperatorSummary(payload), secretValues))
|
||||||
|
}
|
||||||
|
if !payload.Ready {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func workspaceOperatorSummary(payload struct {
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}) string {
|
||||||
|
if payload.Status != "" {
|
||||||
|
return payload.Status
|
||||||
|
}
|
||||||
|
if payload.Ready {
|
||||||
|
return "ready"
|
||||||
|
}
|
||||||
|
return "failed"
|
||||||
|
}
|
||||||
|
|
||||||
func workspaceFailure(stderr io.Writer, err error, secretValues []string) int {
|
func workspaceFailure(stderr io.Writer, err error, secretValues []string) int {
|
||||||
message := output.Sanitize(err.Error(), secretValues)
|
message := output.Sanitize(err.Error(), secretValues)
|
||||||
var operationErr *workspaceops.OperationError
|
var operationErr *workspaceops.OperationError
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package setup
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,6 +15,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||||
@@ -26,6 +29,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var installationIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]*$`)
|
var installationIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]*$`)
|
||||||
|
var secretBundleKeyPattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,127}$`)
|
||||||
|
|
||||||
// atomicWriteNewFile is a seam for failure testing. Its implementation never replaces an existing
|
// atomicWriteNewFile is a seam for failure testing. Its implementation never replaces an existing
|
||||||
// file and leaves no final target until all content is synced.
|
// file and leaves no final target until all content is synced.
|
||||||
@@ -44,6 +48,7 @@ type answers struct {
|
|||||||
secretsFile, piAuthFile string
|
secretsFile, piAuthFile string
|
||||||
gitCredentialsFile, gitCAFile string
|
gitCredentialsFile, gitCAFile string
|
||||||
gitSSHKeyFile, gitKnownHostsFile string
|
gitSSHKeyFile, gitKnownHostsFile string
|
||||||
|
complete bool
|
||||||
createSecretTemplates bool
|
createSecretTemplates bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +121,11 @@ func EnsureFiles(request Request, input io.Reader, output io.Writer) (FilesResul
|
|||||||
if err := validateOrCreateSecretFiles(values, output); err != nil {
|
if err := validateOrCreateSecretFiles(values, output); err != nil {
|
||||||
return FilesResult{}, err
|
return FilesResult{}, err
|
||||||
}
|
}
|
||||||
|
if values.complete {
|
||||||
|
if err := validateCompleteProtectedFiles(values); err != nil {
|
||||||
|
return FilesResult{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
created := make([]string, 0, 2)
|
created := make([]string, 0, 2)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
@@ -163,6 +173,27 @@ func collectAnswers(request Request, input io.Reader, output io.Writer, root str
|
|||||||
value := answersFromRequest(request)
|
value := answersFromRequest(request)
|
||||||
value.installationID = firstNonEmpty(request.InstallationID, os.Getenv("THT_SETUP_INSTALLATION_ID"), "local")
|
value.installationID = firstNonEmpty(request.InstallationID, os.Getenv("THT_SETUP_INSTALLATION_ID"), "local")
|
||||||
value.profile = firstNonEmpty(request.Profile, os.Getenv("THT_SETUP_PROFILE"), "local")
|
value.profile = firstNonEmpty(request.Profile, os.Getenv("THT_SETUP_PROFILE"), "local")
|
||||||
|
value.complete = request.Complete
|
||||||
|
if request.Complete {
|
||||||
|
// The complete path has one predictable protected directory. The user only fills the
|
||||||
|
// bundle and any repository credential that is genuinely required; catalog passwords
|
||||||
|
// are generated below and never appear in the questionnaire.
|
||||||
|
directory := filepath.Join(root, "deploy", value.installationID, "secrets")
|
||||||
|
value.workspaceBranch = firstNonEmpty(value.workspaceBranch, "main")
|
||||||
|
value.secretsFile = firstNonEmpty(value.secretsFile, filepath.Join(directory, "thothii.secrets"))
|
||||||
|
value.piAuthFile = firstNonEmpty(value.piAuthFile, filepath.Join(directory, "pi-auth.json"))
|
||||||
|
if request.NonInteractive {
|
||||||
|
value.workspaceAccess = firstNonEmpty(value.workspaceAccess, accessForRemote(value.workspaceRemote))
|
||||||
|
if value.workspaceAccess == "ssh" {
|
||||||
|
value.gitSSHKeyFile = firstNonEmpty(value.gitSSHKeyFile, filepath.Join(directory, "workspace-git-key"))
|
||||||
|
value.gitKnownHostsFile = firstNonEmpty(value.gitKnownHostsFile, filepath.Join(directory, "workspace-git-known-hosts"))
|
||||||
|
} else {
|
||||||
|
value.gitCredentialsFile = firstNonEmpty(value.gitCredentialsFile, filepath.Join(directory, "workspace-git-credentials"))
|
||||||
|
value.gitCAFile = firstNonEmpty(value.gitCAFile, filepath.Join(directory, "workspace-git-ca.pem"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value.createSecretTemplates = true
|
||||||
|
}
|
||||||
if request.NonInteractive {
|
if request.NonInteractive {
|
||||||
return requireNonInteractiveAnswers(value)
|
return requireNonInteractiveAnswers(value)
|
||||||
}
|
}
|
||||||
@@ -190,6 +221,18 @@ func collectAnswers(request Request, input io.Reader, output io.Writer, root str
|
|||||||
return answers{}, err
|
return answers{}, err
|
||||||
}
|
}
|
||||||
directory := filepath.Join(root, "deploy", value.installationID, "secrets")
|
directory := filepath.Join(root, "deploy", value.installationID, "secrets")
|
||||||
|
if request.Complete {
|
||||||
|
value.secretsFile = firstNonEmpty(value.secretsFile, filepath.Join(directory, "thothii.secrets"))
|
||||||
|
value.piAuthFile = firstNonEmpty(value.piAuthFile, filepath.Join(directory, "pi-auth.json"))
|
||||||
|
if value.workspaceAccess == "ssh" {
|
||||||
|
value.gitSSHKeyFile = firstNonEmpty(value.gitSSHKeyFile, filepath.Join(directory, "workspace-git-key"))
|
||||||
|
value.gitKnownHostsFile = firstNonEmpty(value.gitKnownHostsFile, filepath.Join(directory, "workspace-git-known-hosts"))
|
||||||
|
} else {
|
||||||
|
value.gitCredentialsFile = firstNonEmpty(value.gitCredentialsFile, filepath.Join(directory, "workspace-git-credentials"))
|
||||||
|
value.gitCAFile = firstNonEmpty(value.gitCAFile, filepath.Join(directory, "workspace-git-ca.pem"))
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
if value.secretsFile, err = prompt(scanner, output, "Secret file location", firstNonEmpty(value.secretsFile, filepath.Join(directory, "thothii.secrets"))); err != nil {
|
if value.secretsFile, err = prompt(scanner, output, "Secret file location", firstNonEmpty(value.secretsFile, filepath.Join(directory, "thothii.secrets"))); err != nil {
|
||||||
return answers{}, err
|
return answers{}, err
|
||||||
}
|
}
|
||||||
@@ -216,12 +259,16 @@ func collectAnswers(request Request, input io.Reader, output io.Writer, root str
|
|||||||
return answers{}, missingErr
|
return answers{}, missingErr
|
||||||
}
|
}
|
||||||
if len(missing) > 0 {
|
if len(missing) > 0 {
|
||||||
|
if request.Complete {
|
||||||
|
value.createSecretTemplates = true
|
||||||
|
} else {
|
||||||
answer, promptErr := prompt(scanner, output, "Create blank secret-file templates for the missing locations? Type yes to confirm", "no")
|
answer, promptErr := prompt(scanner, output, "Create blank secret-file templates for the missing locations? Type yes to confirm", "no")
|
||||||
if promptErr != nil {
|
if promptErr != nil {
|
||||||
return answers{}, promptErr
|
return answers{}, promptErr
|
||||||
}
|
}
|
||||||
value.createSecretTemplates = strings.EqualFold(answer, "yes")
|
value.createSecretTemplates = strings.EqualFold(answer, "yes")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +426,13 @@ func render(root, descriptorPath string, value answers) ([]byte, []byte, error)
|
|||||||
if value.llmURL != "" {
|
if value.llmURL != "" {
|
||||||
lines = append(lines, "THT_LLM_URL="+dotenvValue(value.llmURL))
|
lines = append(lines, "THT_LLM_URL="+dotenvValue(value.llmURL))
|
||||||
}
|
}
|
||||||
|
if value.complete {
|
||||||
|
passwordDirectory := filepath.Dir(value.secretsFile)
|
||||||
|
lines = append(lines,
|
||||||
|
"THT_CATALOG_RUNTIME_PASSWORD_SOURCE="+dotenvValue(filepath.Join(passwordDirectory, "catalog-runtime-password")),
|
||||||
|
"THT_CATALOG_MIGRATOR_PASSWORD_SOURCE="+dotenvValue(filepath.Join(passwordDirectory, "catalog-migrator-password")),
|
||||||
|
)
|
||||||
|
}
|
||||||
if value.profile == "server" {
|
if value.profile == "server" {
|
||||||
installationDirectory := filepath.Dir(descriptorPath)
|
installationDirectory := filepath.Dir(descriptorPath)
|
||||||
lines = append(lines,
|
lines = append(lines,
|
||||||
@@ -474,10 +528,7 @@ func validateOrCreateSecretFiles(value answers, output io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(missing) == 0 {
|
if len(missing) > 0 && !value.createSecretTemplates {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !value.createSecretTemplates {
|
|
||||||
return fmt.Errorf("secret files are missing: %s; create them yourself or explicitly confirm blank secret-file templates", strings.Join(missing, ", "))
|
return fmt.Errorf("secret files are missing: %s; create them yourself or explicitly confirm blank secret-file templates", strings.Join(missing, ", "))
|
||||||
}
|
}
|
||||||
for _, path := range missing {
|
for _, path := range missing {
|
||||||
@@ -489,6 +540,104 @@ func validateOrCreateSecretFiles(value answers, output io.Writer) error {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(output, "Created blank secret-file template: %s\n", path)
|
fmt.Fprintf(output, "Created blank secret-file template: %s\n", path)
|
||||||
}
|
}
|
||||||
|
if value.complete {
|
||||||
|
for _, path := range catalogPasswordPaths(value) {
|
||||||
|
exists, err := inspectExistingSecretFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents, err := generatedCatalogPassword()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("generate catalog password: %w", err)
|
||||||
|
}
|
||||||
|
if err := atomicWriteNewFile(path, contents, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("create catalog password %s: %w", path, err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(output, "Created generated catalog password file: %s\n", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogPasswordPaths(value answers) []string {
|
||||||
|
directory := filepath.Dir(value.secretsFile)
|
||||||
|
return []string{
|
||||||
|
filepath.Join(directory, "catalog-runtime-password"),
|
||||||
|
filepath.Join(directory, "catalog-migrator-password"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedCatalogPassword() ([]byte, error) {
|
||||||
|
value := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(value); err != nil {
|
||||||
|
return nil, errors.New("secure random source is unavailable")
|
||||||
|
}
|
||||||
|
return []byte(hex.EncodeToString(value) + "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCompleteProtectedFiles(value answers) error {
|
||||||
|
if err := validateSecretBundle(value.secretsFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The generated catalog deliberately uses Pi's built-in provider. A syntactically empty
|
||||||
|
// auth store would let Docker start only to fail at the first provider check, so catch it
|
||||||
|
// before any image is built. Other model providers can be selected later in the descriptor.
|
||||||
|
contents, err := safeio.ReadCanonicalRegular(value.piAuthFile, maxSecretBytes)
|
||||||
|
if err != nil || strings.TrimSpace(string(contents)) == "" || strings.TrimSpace(string(contents)) == "{}" {
|
||||||
|
return fmt.Errorf("complete setup requires usable Pi credentials in %s", value.piAuthFile)
|
||||||
|
}
|
||||||
|
if value.workspaceAccess == "ssh" {
|
||||||
|
for name, path := range map[string]string{
|
||||||
|
"workspace Git SSH key": value.gitSSHKeyFile,
|
||||||
|
"workspace Git known-hosts": value.gitKnownHostsFile,
|
||||||
|
} {
|
||||||
|
contents, readErr := safeio.ReadCanonicalRegular(path, maxSecretBytes)
|
||||||
|
if readErr != nil || strings.TrimSpace(string(contents)) == "" {
|
||||||
|
return fmt.Errorf("complete setup requires usable %s in %s", name, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for name, path := range map[string]string{
|
||||||
|
"workspace Git credentials": value.gitCredentialsFile,
|
||||||
|
"workspace Git CA": value.gitCAFile,
|
||||||
|
} {
|
||||||
|
contents, readErr := safeio.ReadCanonicalRegular(path, maxSecretBytes)
|
||||||
|
if readErr != nil || strings.TrimSpace(string(contents)) == "" {
|
||||||
|
return fmt.Errorf("complete setup requires usable %s in %s", name, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSecretBundle(path string) error {
|
||||||
|
contents, err := safeio.ReadCanonicalRegular(path, maxSecretBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("complete setup cannot read the secret bundle %s", path)
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
for lineNumber, raw := range strings.Split(string(contents), "\n") {
|
||||||
|
line := strings.TrimSuffix(raw, "\r")
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, secret, found := strings.Cut(line, "=")
|
||||||
|
invalid := !found || !secretBundleKeyPattern.MatchString(key) || strings.TrimSpace(key) != key ||
|
||||||
|
secret == "" || strings.TrimSpace(secret) != secret ||
|
||||||
|
strings.Contains(strings.ToLower(secret), "replace-me") ||
|
||||||
|
strings.IndexFunc(secret, unicode.IsSpace) >= 0
|
||||||
|
if invalid {
|
||||||
|
return fmt.Errorf("complete setup found an invalid secret bundle entry at line %d", lineNumber+1)
|
||||||
|
}
|
||||||
|
if _, duplicate := seen[key]; duplicate {
|
||||||
|
return fmt.Errorf("complete setup found a duplicate secret bundle key %s", key)
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,50 @@ func TestEnsureFilesRequiresExplicitNonInteractiveAnswers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteSetupCreatesProtectedPlaceholdersThenRequiresUsableCredentials(t *testing.T) {
|
||||||
|
root := newProject(t, "complete setup")
|
||||||
|
for name, value := range map[string]string{
|
||||||
|
"THT_SETUP_WORKSPACE_REMOTE": "git@git.example.invalid:team/workspaces.git",
|
||||||
|
"THT_SETUP_WORKSPACE_BRANCH": "main",
|
||||||
|
"THT_SETUP_WORKSPACE_ACCESS": "ssh",
|
||||||
|
} {
|
||||||
|
t.Setenv(name, value)
|
||||||
|
}
|
||||||
|
request := Request{ProjectRoot: root, InstallationID: "local", Profile: "local", Complete: true, NonInteractive: true}
|
||||||
|
if _, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{}); err == nil || !strings.Contains(err.Error(), "usable Pi credentials") {
|
||||||
|
t.Fatalf("first complete setup error = %v, want the placeholder guidance", err)
|
||||||
|
}
|
||||||
|
secretRoot := filepath.Join(root, "deploy", "local", "secrets")
|
||||||
|
for path, contents := range map[string]string{
|
||||||
|
filepath.Join(secretRoot, "pi-auth.json"): "{\"deepseek\":{\"apiKey\":\"configured\"}}\n",
|
||||||
|
filepath.Join(secretRoot, "workspace-git-key"): "private-key\n",
|
||||||
|
filepath.Join(secretRoot, "workspace-git-known-hosts"): "git.example.invalid ssh-ed25519 AAAA\n",
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
environment, err := os.ReadFile(result.EnvironmentPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"THT_CATALOG_RUNTIME_PASSWORD_SOURCE", "THT_CATALOG_MIGRATOR_PASSWORD_SOURCE"} {
|
||||||
|
if !strings.Contains(string(environment), name+"=") {
|
||||||
|
t.Fatalf("complete environment misses %s: %s", name, environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range []string{"catalog-runtime-password", "catalog-migrator-password"} {
|
||||||
|
contents, readErr := os.ReadFile(filepath.Join(secretRoot, name))
|
||||||
|
if readErr != nil || len(strings.TrimSpace(string(contents))) < 32 {
|
||||||
|
t.Fatalf("generated catalog password %s is unavailable or too short", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnsureFilesIncludesServerStorageLocations(t *testing.T) {
|
func TestEnsureFilesIncludesServerStorageLocations(t *testing.T) {
|
||||||
requireProjectedServerTestHost(t)
|
requireProjectedServerTestHost(t)
|
||||||
root := newProject(t, "server profile")
|
root := newProject(t, "server profile")
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
// Package setup creates the local, non-secret configuration selected by tht setup.
|
// Package setup creates the local, non-secret configuration selected by tht setup.
|
||||||
package setup
|
package setup
|
||||||
|
|
||||||
// Request contains the stable setup-file inputs. Task 5 will use ConfigureOnly when it adds
|
// Request contains the stable setup-file inputs.
|
||||||
// Compose validation and lifecycle orchestration.
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ProjectRoot string
|
ProjectRoot string
|
||||||
InstallationID string
|
InstallationID string
|
||||||
Profile string
|
Profile string
|
||||||
|
// Complete runs the installation-only steps that are safe to automate: catalog migration,
|
||||||
|
// stack startup, and the initial workspace pull. It intentionally does not invent database
|
||||||
|
// bindings or credentials that belong to the installation operator.
|
||||||
|
Complete bool
|
||||||
ConfigureOnly bool
|
ConfigureOnly bool
|
||||||
NonInteractive bool
|
NonInteractive bool
|
||||||
Answers Answers
|
Answers Answers
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package setup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -35,7 +36,8 @@ type Result struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run validates the host, creates or validates non-secret configuration, and by default builds,
|
// Run validates the host, creates or validates non-secret configuration, and by default builds,
|
||||||
// starts, and verifies the current checkout. ConfigureOnly stops after Compose rendering.
|
// starts, and verifies the current checkout. Complete additionally migrates the Catalog and
|
||||||
|
// imports the configured workspace repository. ConfigureOnly stops after Compose rendering.
|
||||||
func Run(ctx context.Context, runner compose.Runner, request Request, input io.Reader, output io.Writer) (Result, error) {
|
func Run(ctx context.Context, runner compose.Runner, request Request, input io.Reader, output io.Writer) (Result, error) {
|
||||||
if runner == nil {
|
if runner == nil {
|
||||||
return Result{}, errors.New("setup requires a Docker command runner")
|
return Result{}, errors.New("setup requires a Docker command runner")
|
||||||
@@ -71,13 +73,33 @@ func Run(ctx context.Context, runner compose.Runner, request Request, input io.R
|
|||||||
fmt.Fprintf(output, "Configuration is ready: %s\n", result.DescriptorPath)
|
fmt.Fprintf(output, "Configuration is ready: %s\n", result.DescriptorPath)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
if err := service.Start(ctx, installation, runner, true); err != nil {
|
if request.Complete {
|
||||||
|
if err := runCompose(ctx, runner, installation, "build"); err != nil {
|
||||||
|
return Result{}, fmt.Errorf("setup image build: %w", err)
|
||||||
|
}
|
||||||
|
if err := runCompose(ctx, runner, installation, "up", "--detach", "catalog-db"); err != nil {
|
||||||
|
return Result{}, fmt.Errorf("setup Catalog database start: %w", err)
|
||||||
|
}
|
||||||
|
if err := runCompose(ctx, runner, installation,
|
||||||
|
"--profile", "catalog-maintenance", "run", "--rm", "catalog-migrate"); err != nil {
|
||||||
|
return Result{}, fmt.Errorf("setup Catalog migration: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := service.Start(ctx, installation, runner, !request.Complete); err != nil {
|
||||||
if strings.Contains(err.Error(), "image build") {
|
if strings.Contains(err.Error(), "image build") {
|
||||||
return Result{}, fmt.Errorf("setup %w", err)
|
return Result{}, fmt.Errorf("setup %w", err)
|
||||||
}
|
}
|
||||||
return Result{}, withStartupRecovery(fmt.Errorf("setup %w", err), recoveryService(err))
|
return Result{}, withStartupRecovery(fmt.Errorf("setup %w", err), recoveryService(err))
|
||||||
}
|
}
|
||||||
result.Built, result.Started, result.Healthy = true, true, true
|
result.Built, result.Started, result.Healthy = true, true, true
|
||||||
|
if request.Complete {
|
||||||
|
if err := runOperator(ctx, runner, installation, "workspace-pull"); err != nil {
|
||||||
|
return Result{}, withStartupRecovery(fmt.Errorf("setup workspace import: %w", err), "core")
|
||||||
|
}
|
||||||
|
if err := runOperator(ctx, runner, installation, "pi-test"); err != nil {
|
||||||
|
return Result{}, withStartupRecovery(fmt.Errorf("setup LLM credential test: %w", err), "core")
|
||||||
|
}
|
||||||
|
}
|
||||||
report, err := doctor.Run(ctx, installation, runner)
|
report, err := doctor.Run(ctx, installation, runner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Result{}, withStartupRecovery(fmt.Errorf("setup doctor: %w", err), "core")
|
return Result{}, withStartupRecovery(fmt.Errorf("setup doctor: %w", err), "core")
|
||||||
@@ -85,6 +107,9 @@ func Run(ctx context.Context, runner compose.Runner, request Request, input io.R
|
|||||||
if !report.OK {
|
if !report.OK {
|
||||||
return Result{}, withStartupRecovery(errors.New("setup doctor reported failed checks"), "core")
|
return Result{}, withStartupRecovery(errors.New("setup doctor reported failed checks"), "core")
|
||||||
}
|
}
|
||||||
|
if request.Complete {
|
||||||
|
fmt.Fprintln(output, "Workspace repository pulled and activated; run 'tht workspace test' after configuring each workspace database.")
|
||||||
|
}
|
||||||
fmt.Fprintf(output, "ThothII is ready at %s\nInstallation descriptor: %s\nNext: tht status\n", frontendURL(installation), result.DescriptorPath)
|
fmt.Fprintf(output, "ThothII is ready at %s\nInstallation descriptor: %s\nNext: tht status\n", frontendURL(installation), result.DescriptorPath)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -219,6 +244,25 @@ func runCompose(ctx context.Context, runner compose.Runner, installation config.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runOperator(ctx context.Context, runner compose.Runner, installation config.Installation, action string) error {
|
||||||
|
result, err := runner.Run(ctx, installation.ComposeArgs(
|
||||||
|
"exec", "-T", "core", "node", "dist/operator-command.js", action,
|
||||||
|
), nil)
|
||||||
|
if err != nil {
|
||||||
|
if result.ExitCode != 0 {
|
||||||
|
return fmt.Errorf("Docker exited with status %d", result.ExitCode)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Ready *bool `json:"ready"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(result.Stdout), &payload); err != nil || payload.Ready == nil || !*payload.Ready {
|
||||||
|
return fmt.Errorf("operator action %s reported failure", action)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func composeFailure(result compose.Result, cause error) error {
|
func composeFailure(result compose.Result, cause error) error {
|
||||||
if result.ExitCode != 0 {
|
if result.ExitCode != 0 {
|
||||||
return fmt.Errorf("Docker exited with status %d", result.ExitCode)
|
return fmt.Errorf("Docker exited with status %d", result.ExitCode)
|
||||||
|
|||||||
@@ -68,6 +68,34 @@ func TestRunConfigureOnlyStopsAfterRenderedConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunCompleteMigratesCatalogPullsWorkspaceAndTestsLLM(t *testing.T) {
|
||||||
|
root, request := setupRunFixture(t, false)
|
||||||
|
request.Complete = true
|
||||||
|
secretRoot := filepath.Join(root, "deploy", "ci", "secrets")
|
||||||
|
for path, contents := range map[string]string{
|
||||||
|
filepath.Join(secretRoot, "pi-auth.json"): "{\"deepseek\":{\"apiKey\":\"configured\"}}\n",
|
||||||
|
filepath.Join(secretRoot, "workspace-git-key"): "private-key\n",
|
||||||
|
filepath.Join(secretRoot, "workspace-git-known-hosts"): "git.example.invalid ssh-ed25519 AAAA\n",
|
||||||
|
} {
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runner := &setupRunner{health: []string{healthyServicesJSON}}
|
||||||
|
if _, err := Run(context.Background(), runner, request, strings.NewReader(""), io.Discard); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []string{
|
||||||
|
"docker engine", "docker compose", "architecture", "compose config", "compose build",
|
||||||
|
"catalog db", "catalog migrate", "compose up", "health", "workspace pull", "pi doctor",
|
||||||
|
"doctor docker", "doctor compose", "compose config", "doctor config", "health",
|
||||||
|
"authentication", "core HTTP", "frontend HTTP", "workspace registry", "workflow doctor", "pi doctor",
|
||||||
|
}
|
||||||
|
if got := collapseStages(runner.stages); strings.Join(got, " | ") != strings.Join(want, " | ") {
|
||||||
|
t.Fatalf("complete setup stages = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunConfiguresAndStaticallyValidatesLocalAuthBeforeComposeRender(t *testing.T) {
|
func TestRunConfiguresAndStaticallyValidatesLocalAuthBeforeComposeRender(t *testing.T) {
|
||||||
projectRoot, request := setupRunFixture(t, true)
|
projectRoot, request := setupRunFixture(t, true)
|
||||||
passwordFile := filepath.Join(projectRoot, "initial-admin-password")
|
passwordFile := filepath.Join(projectRoot, "initial-admin-password")
|
||||||
@@ -421,6 +449,10 @@ func setupStage(args []string) (string, compose.Result) {
|
|||||||
return "architecture", compose.Result{Stdout: "arm64\n"}
|
return "architecture", compose.Result{Stdout: "arm64\n"}
|
||||||
case strings.HasSuffix(joined, " config --quiet"):
|
case strings.HasSuffix(joined, " config --quiet"):
|
||||||
return "compose config", compose.Result{}
|
return "compose config", compose.Result{}
|
||||||
|
case strings.HasSuffix(joined, " up --detach catalog-db"):
|
||||||
|
return "catalog db", compose.Result{}
|
||||||
|
case strings.HasSuffix(joined, " --profile catalog-maintenance run --rm catalog-migrate"):
|
||||||
|
return "catalog migrate", compose.Result{}
|
||||||
case strings.HasSuffix(joined, " build"):
|
case strings.HasSuffix(joined, " build"):
|
||||||
return "compose build", compose.Result{}
|
return "compose build", compose.Result{}
|
||||||
case strings.HasSuffix(joined, " up --detach --remove-orphans"):
|
case strings.HasSuffix(joined, " up --detach --remove-orphans"):
|
||||||
@@ -433,6 +465,8 @@ func setupStage(args []string) (string, compose.Result) {
|
|||||||
return "authentication", compose.Result{Stdout: `{"ready":true,"mode":"oidc","checks":[{"level":"info","code":"auth_ready","message":"Authentication is ready."}]}`}
|
return "authentication", compose.Result{Stdout: `{"ready":true,"mode":"oidc","checks":[{"level":"info","code":"auth_ready","message":"Authentication is ready."}]}`}
|
||||||
case strings.Contains(joined, "exec -T core node dist/operator-command.js workflow-doctor"):
|
case strings.Contains(joined, "exec -T core node dist/operator-command.js workflow-doctor"):
|
||||||
return "workflow doctor", compose.Result{Stdout: `{"ready":true,"workspaces":1}`}
|
return "workflow doctor", compose.Result{Stdout: `{"ready":true,"workspaces":1}`}
|
||||||
|
case strings.Contains(joined, "operator-command.js workspace-pull"):
|
||||||
|
return "workspace pull", compose.Result{Stdout: `{"ready":true,"status":"succeeded"}`}
|
||||||
case strings.Contains(joined, "exec -T core curl -fsS --max-time 5 http://127.0.0.1:8787/health"):
|
case strings.Contains(joined, "exec -T core curl -fsS --max-time 5 http://127.0.0.1:8787/health"):
|
||||||
return "core HTTP", compose.Result{}
|
return "core HTTP", compose.Result{}
|
||||||
case strings.Contains(joined, "exec -T frontend wget -q -T 5 -O /dev/null http://127.0.0.1:8080/"):
|
case strings.Contains(joined, "exec -T frontend wget -q -T 5 -O /dev/null http://127.0.0.1:8080/"):
|
||||||
|
|||||||
Reference in New Issue
Block a user