Files
ThothII/docs/install/local.md
T

22 KiB

Install ThothII on a local PC or Mac

This guide installs one loopback-only ThothII on the same Windows, macOS, or Linux computer that runs Docker. The supported application is one Docker Compose distribution containing exactly frontend and core; Pi is pinned inside core. DWH, vector database, embedding, and LLM remain external configurable services even when they run on this computer.

No host Pi, Node.js, Python, Go toolchain, Docker socket in core, or browser shell is required. Commands that contain example paths must be changed to absolute paths on your computer.

Choose your platform

  • macOS: use Terminal and Docker Desktop. Apple Silicon and Intel are supported by the local image build.
  • Windows PowerShell: use Docker Desktop with its WSL2 engine, Git for Windows, the Windows build launcher, and tht-windows-amd64.exe.
  • Windows WSL2 (recommended): enable Docker Desktop integration for your Linux distribution, clone under /home/<user> rather than /mnt/c, and follow the Linux shell commands.
  • Linux PC: use Docker Engine plus the Compose v2 plugin and the Linux tht binary.

Windows users must also read Windows and WSL2 line endings before the first build.

Prerequisites

Install only:

  1. Git 2.39 or newer.
  2. Docker Desktop on macOS/Windows, or Docker Engine on Linux.
  3. Docker Compose v2 (docker compose, not legacy docker-compose).
  4. About 10 GB of free disk for source, images, build cache, and initial volumes.
  5. Network access to the workspace Git remote and configured DWH/vector/embedding/LLM endpoints.

Verify the tools:

git --version
docker version
docker compose version
docker run --rm hello-world

On Linux, add the operator to the Docker group only if local policy permits it; sign out and back in afterward. A local installation needs no inbound firewall rule because ports bind only to 127.0.0.1.

Clone and verify LF

Use a git clone command that disables automatic CRLF conversion for this checkout.

macOS and Linux:

git -c core.autocrlf=false clone https://github.example.invalid/your-org/ThothII.git
cd ThothII
git config --local core.autocrlf false
bash scripts/verify-line-endings.sh

Windows PowerShell:

git -c core.autocrlf=false clone https://github.example.invalid/your-org/ThothII.git
Set-Location ThothII
git config --local core.autocrlf false
& "C:\Program Files\Git\bin\bash.exe" scripts/verify-line-endings.sh

Windows WSL2:

mkdir -p "$HOME/src" && cd "$HOME/src"
git -c core.autocrlf=false clone https://github.example.invalid/your-org/ThothII.git
cd ThothII
git config --local core.autocrlf false
bash scripts/verify-line-endings.sh

Stop if the verifier names any path. Do not build from a CRLF checkout.

Create the local operator files

Copy the non-secret template. This untracked .env contains addresses and absolute source paths, never secret values:

cp deploy/env/local.env.example deploy/env/local.env
mkdir -p /absolute/path/to/thothii-operator/secrets
chmod 0700 /absolute/path/to/thothii-operator/secrets

Native Windows PowerShell performs the same setup without POSIX utilities. The ACL commands remove inherited access from the new operator directory and grant full control only to the current Windows identity. Stop if either icacls.exe command returns a nonzero exit code:

$OperatorDir = Join-Path $env:USERPROFILE 'thothii-operator'
$SecretsDir = Join-Path $OperatorDir 'secrets'
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
if (Test-Path $OperatorDir) { throw 'Use a new operator directory or review its ACLs manually.' }
New-Item -ItemType Directory -Force -Path $OperatorDir, $SecretsDir | Out-Null
icacls.exe $OperatorDir /inheritance:r
if ($LASTEXITCODE -ne 0) { throw 'Could not remove inherited operator-directory ACLs.' }
icacls.exe $OperatorDir /grant:r "${CurrentUser}:(OI)(CI)F"
if ($LASTEXITCODE -ne 0) { throw 'Could not grant the current user the operator-directory ACL.' }
Copy-Item deploy/env/local.env.example deploy/env/local.env
Copy-Item docs/install/examples/thothii-installation.local.yaml `
  (Join-Path $OperatorDir 'thothii-installation.yaml')

Edit deploy/env/local.env. At minimum set the workspace Git remote, PI_AUTH_FILE, THT_SECRETS_FILE, and external service endpoints. Create the Pi/application and Git transport files under the protected operator directory and set mode 0600. On Windows use a user-only ACL instead. DWH and Evidence credentials are entered later through Workspace management and stored in the backend's encrypted workspace-secrets volume.

Do not paste credentials into this guide's commands, .env, workspace YAML, Git, URLs, image build arguments, or the installation descriptor. Secret contents are mounted read-only under /run/secrets (Pi's auth store has its own protected read-only mount) and must never be committed, embedded, rendered, or logged.

Follow the local workspace repository guide to choose exactly one read-only Git SSH/HTTPS override. A fresh install requires a valid private workspace repository; the remote Git repository remains the source of truth.

Copy the installation example to an operator-controlled file named exactly thothii-installation.yaml, then replace all placeholders with absolute paths:

cp docs/install/examples/thothii-installation.local.yaml \
  /absolute/path/to/thothii-operator/thothii-installation.yaml

For HTTPS, replace the SSH override in that file with deploy/compose.git-https.yaml. Add only reviewed local overrides. Paths may contain spaces when correctly represented as YAML strings.

Native Windows uses the same four fields. Use single-quoted absolute Windows paths so backslashes remain literal YAML characters:

profile: local
projectDirectory: 'C:\Users\operator\src\ThothII'
envFile: 'C:\Users\operator\src\ThothII\deploy\env\local.env'
overrides:
  - 'C:\Users\operator\src\ThothII\deploy\compose.git-ssh.yaml'

Address external services

An address is interpreted inside core. Therefore container 127.0.0.1 means the container itself, not the Docker host. Keep external DWH and LLM addresses configurable in the installation; Qdrant and embedding are internal services in the standard stack.

  • Docker Desktop (macOS and Windows): use host.docker.internal, for example http://host.docker.internal:11434.
  • Linux: if a service runs on the host, create an untracked override and include its absolute path in thothii-installation.yaml:
services:
  core:
    extra_hosts:
      - "host.docker.internal:host-gateway"

Then use host.docker.internal in the endpoint. extra_hosts: host.docker.internal:host-gateway is a host routing aid, not a bundled service. Prefer a real DNS name for independently operated services; retain TLS and authentication even when co-located.

Build ThothII and tht

The canonical local Compose smoke uses the base file plus the local profile. Keep this exact base+profile command available for install verification:

docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up --build -d

After the stack is ready, configure and check authentication with the single host CLI tht; see the local authentication guide. Authentication configuration is installation-global and is checked before workspace tests.

From the repository root, macOS/Linux/WSL2 users run:

bash scripts/build-local.sh
bash scripts/build-tht.sh

Native PowerShell users run:

powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1
& "C:\Program Files\Git\bin\bash.exe" scripts/build-tht.sh

The second command uses Docker to create native operator binaries under dist/tht; users do not need to know or install Go. Select tht-darwin-arm64 or -amd64 on macOS, tht-linux-amd64 or -arm64 on Linux/WSL2, and tht-windows-amd64.exe on Windows. Copy the selected file to the protected operator directory and, on macOS/Linux, run chmod 0755 on it.

Start and verify

Set convenient variables (PowerShell users use $THT_BIN and $INSTALLATION with & $THT_BIN): Every operator call has the form tht --installation <absolute-descriptor> <command>.

THT_BIN=/absolute/path/to/thothii-operator/tht
INSTALLATION=/absolute/path/to/thothii-operator/thothii-installation.yaml
"$THT_BIN" --installation "$INSTALLATION" update --check-only
"$THT_BIN" --installation "$INSTALLATION" start
"$THT_BIN" --installation "$INSTALLATION" status
"$THT_BIN" --installation "$INSTALLATION" doctor

Native PowerShell uses the same order:

$THT_BIN = 'C:\Users\operator\thothii-operator\tht.exe'
$INSTALLATION = 'C:\Users\operator\thothii-operator\thothii-installation.yaml'
& $THT_BIN --installation $INSTALLATION update --check-only
& $THT_BIN --installation $INSTALLATION start
& $THT_BIN --installation $INSTALLATION status
& $THT_BIN --installation $INSTALLATION doctor

Wait for both services, then check the same-origin frontend and direct loopback core:

curl --fail http://127.0.0.1:8080/health
curl --fail http://127.0.0.1:8787/health
"$THT_BIN" --installation "$INSTALLATION" pi doctor
"$THT_BIN" --installation "$INSTALLATION" pi test

Native PowerShell must call curl.exe explicitly; Windows PowerShell may otherwise resolve curl to Invoke-WebRequest:

curl.exe --fail --silent --show-error http://127.0.0.1:8080/health
curl.exe --fail --silent --show-error http://127.0.0.1:8787/health
& $THT_BIN --installation $INSTALLATION pi doctor
& $THT_BIN --installation $INSTALLATION pi test

Open http://127.0.0.1:8080. If a check fails, run tht ... logs or pi logs; these are bounded and sanitize declared secrets. Do not publish either loopback port.

Update an installation

Commit or back up local operator changes first and finish active sessions. A promoted Pi image is selected by the durable, installation-specific current-image.yaml after every base/profile file. Therefore rebuilding thothii-core:local followed by update --check-only does not reconcile a previous pi update: the old promoted core would remain selected.

Do not delete or edit the selector. tht status is the installation-aware selector test. If the running core image is the base thothii-core:local image, no Pi update has promoted a durable lifecycle image and an ordinary same-Pi-version source rebuild/start is supported. If status shows a lifecycle image and the pulled Pi pin is unchanged, pi update would be a no-op and the procedure must stop. A changed Pi pin uses transactional pi update --source build in either case.

macOS, Linux, and WSL2:

set -euo pipefail

abort_update() { printf 'Source update stopped: %s\n' "$1" >&2; exit 1; }
require_clean_source() {
  local source_state
  if ! source_state="$(git status --porcelain --untracked-files=all)"; then
    abort_update "git status failed"
  fi
  [[ -z "$source_state" ]] || abort_update "commit, remove, or back up every tracked/untracked source change"
}

require_clean_source
if ! git pull --ff-only; then abort_update "git pull --ff-only failed"; fi
require_clean_source
if ! git config --local core.autocrlf false; then abort_update "could not set repository LF policy"; fi
if ! bash scripts/verify-line-endings.sh; then abort_update "the pulled checkout contains CRLF files"; fi
if ! SOURCE_REVISION="$(git rev-parse HEAD)"; then abort_update "could not record the pulled revision"; fi
if ! NEXT_PI_VERSION="$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)"; then
  abort_update "could not read the pulled Pi pin"
fi
[[ -n "$NEXT_PI_VERSION" && "$NEXT_PI_VERSION" != *$'\n'* ]] || abort_update "expected one pinned default PI_VERSION"
if ! INSTALLATION_STATUS="$("$THT_BIN" --installation "$INSTALLATION" status)"; then
  abort_update "tht status failed"
fi
if ! RUNNING_PI_VERSION="$("$THT_BIN" --installation "$INSTALLATION" pi status)"; then
  abort_update "tht pi status failed"
fi
RUNNING_PI_VERSION="${RUNNING_PI_VERSION#Pi version: }"
[[ -n "$RUNNING_PI_VERSION" ]] || abort_update "tht pi status returned no version"

COMPACT_STATUS="${INSTALLATION_STATUS//[[:space:]]/}"
USES_BASE_CORE=false
if [[ "$COMPACT_STATUS" == *'"Image":"thothii-core:local"'* ]]; then
  USES_BASE_CORE=true
fi
TRANSACTIONAL_PI_UPDATE=true
if [[ "$NEXT_PI_VERSION" == "$RUNNING_PI_VERSION" ]]; then
  [[ "$USES_BASE_CORE" == true ]] || abort_update "same Pi version is selected by a durable lifecycle image"
  TRANSACTIONAL_PI_UPDATE=false
fi

if ! bash scripts/build-local.sh; then abort_update "the local image build failed"; fi
if ! bash scripts/build-tht.sh; then abort_update "the tht build failed"; fi
if ! "$THT_BIN" --installation "$INSTALLATION" update --check-only; then
  abort_update "the installation render check failed"
fi
if [[ "$TRANSACTIONAL_PI_UPDATE" == true ]]; then
  if ! "$THT_BIN" --installation "$INSTALLATION" pi update \
    --version "$NEXT_PI_VERSION" --source build --yes --drain; then
    abort_update "the transactional core update failed"
  fi
fi
if ! "$THT_BIN" --installation "$INSTALLATION" start; then abort_update "installation start failed"; fi
if ! curl --fail http://127.0.0.1:8080/health; then abort_update "frontend health check failed"; fi
if ! curl --fail http://127.0.0.1:8787/health; then abort_update "core health check failed"; fi
if ! FINAL_STATUS="$("$THT_BIN" --installation "$INSTALLATION" status)"; then abort_update "final status failed"; fi
if ! FINAL_PI_STATUS="$("$THT_BIN" --installation "$INSTALLATION" pi status)"; then abort_update "final pi status failed"; fi
[[ "${FINAL_PI_STATUS#Pi version: }" == "$NEXT_PI_VERSION" ]] || abort_update "running Pi version does not match the pulled pin"
if ! "$THT_BIN" --installation "$INSTALLATION" doctor; then abort_update "final doctor failed"; fi
require_clean_source
printf 'Built source revision: %s\n%s\n%s\n' "$SOURCE_REVISION" "$FINAL_STATUS" "$FINAL_PI_STATUS"

Native Windows PowerShell uses the same fail-closed version comparison and transactional promotion:

$ErrorActionPreference = 'Stop'
function Assert-NativeSuccess([string]$Step) {
  if ($LASTEXITCODE -ne 0) { throw "$Step failed with exit code $LASTEXITCODE." }
}
function Assert-CleanSource {
  $SourceState = @(git status --porcelain --untracked-files=all)
  Assert-NativeSuccess 'git status'
  if ($SourceState.Count -ne 0) {
    throw 'Commit, remove, or back up every tracked/untracked source change.'
  }
}

Assert-CleanSource
git pull --ff-only
Assert-NativeSuccess 'source pull'
Assert-CleanSource
git config --local core.autocrlf false
Assert-NativeSuccess 'repository LF policy'
& "C:\Program Files\Git\bin\bash.exe" scripts/verify-line-endings.sh
Assert-NativeSuccess 'pulled checkout LF verification'
$SourceRevision = git rev-parse HEAD
Assert-NativeSuccess 'source revision read'
$VersionLine = @(Select-String -Path docker/core.Dockerfile -Pattern '^ARG PI_VERSION=(.+)$')
if ($VersionLine.Count -ne 1) { throw 'Expected exactly one pinned default PI_VERSION.' }
$NextPiVersion = $VersionLine.Matches[0].Groups[1].Value
$InstallationStatus = @(& $THT_BIN --installation $INSTALLATION status)
Assert-NativeSuccess 'installation status'
$RunningPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)
Assert-NativeSuccess 'Pi status'
$RunningPiVersion = $RunningPiStatus -replace '^Pi version:\s*', ''
if ([string]::IsNullOrWhiteSpace($RunningPiVersion)) { throw 'Pi status returned no version.' }
$Services = $InstallationStatus | ConvertFrom-Json
$CoreServices = @($Services | Where-Object { $_.Service -eq 'core' })
if ($CoreServices.Count -ne 1) { throw 'Installation status did not identify exactly one core service.' }
$UsesBaseCore = $CoreServices[0].Image -eq 'thothii-core:local'
$TransactionalPiUpdate = $true
if ($NextPiVersion -eq $RunningPiVersion) {
  if (-not $UsesBaseCore) { throw 'Same Pi version is selected by a durable lifecycle image.' }
  $TransactionalPiUpdate = $false
}
powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1
Assert-NativeSuccess 'local image build'
& "C:\Program Files\Git\bin\bash.exe" scripts/build-tht.sh
Assert-NativeSuccess 'tht build'
& $THT_BIN --installation $INSTALLATION update --check-only
Assert-NativeSuccess 'installation render check'
if ($TransactionalPiUpdate) {
  & $THT_BIN --installation $INSTALLATION pi update `
    --version $NextPiVersion --source build --yes --drain
  Assert-NativeSuccess 'transactional core update'
}
& $THT_BIN --installation $INSTALLATION start
Assert-NativeSuccess 'installation start'
curl.exe --fail --silent --show-error http://127.0.0.1:8080/health
Assert-NativeSuccess 'frontend health check'
curl.exe --fail --silent --show-error http://127.0.0.1:8787/health
Assert-NativeSuccess 'core health check'
$FinalStatus = @(& $THT_BIN --installation $INSTALLATION status)
Assert-NativeSuccess 'final installation status'
$FinalPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)
Assert-NativeSuccess 'final Pi status'
if (($FinalPiStatus -replace '^Pi version:\s*', '') -ne $NextPiVersion) {
  throw 'Running Pi version does not match the pulled pin.'
}
& $THT_BIN --installation $INSTALLATION doctor
Assert-NativeSuccess 'final doctor'
Assert-CleanSource
Write-Output "Built source revision: $SourceRevision"
Write-Output $FinalStatus
Write-Output $FinalPiStatus

The revision is printed only after every source/build/start/health/installation-aware check passes and a final porcelain check still reports no tracked or untracked source changes. For a changed Pi pin, status reports the promoted lifecycle candidate; for a same-version installation with no selector, status reports the rebuilt base core. update --check-only alone proves only that Compose renders. Review release notes before updating. See Pi management for rollback; never install a package in the running container.

Back up and restore

Back up before source/Pi updates and test restoration periodically. First stop cleanly:

"$THT_BIN" --installation "$INSTALLATION" stop
docker volume ls --format '{{.Name}}' | grep '^thothii-'

Identify the four exact volumes belonging to this installation: settings, pi-state, workspace-registry, and sessions. Confirm their Compose project label with docker volume inspect. For each exact volume, archive it to a protected backup directory:

BACKUP_DIR=/absolute/path/to/backups/2026-08-05
VOLUME=exact-installation-volume-name
mkdir -p "$BACKUP_DIR"
docker run --rm -v "$VOLUME:/source:ro" -v "$BACKUP_DIR:/backup" \
  alpine:3.22 tar -C /source -czf "/backup/$VOLUME.tgz" .

Native PowerShell can run the same read-only archive container:

$BackupDir = 'C:\Users\operator\thothii-backups\2026-08-05'
$Volume = 'exact-installation-volume-name'
New-Item -ItemType Directory -Force $BackupDir | Out-Null
docker run --rm -v "${Volume}:/source:ro" -v "${BackupDir}:/backup" `
  alpine:3.22 tar -C /source -czf "/backup/${Volume}.tgz" .

Also back up the installation descriptor, operator environment, generated overrides, and secret files to separate encrypted/protected storage. Never commit them. Record image digests and the Git revision. Do not back up while containers are running.

Restore only while stopped and only into a new, verified-empty exact target volume. Test the archive in a disposable installation first:

TARGET_VOLUME=exact-empty-target-volume-name
ARCHIVE=/absolute/path/to/backups/2026-08-05/exact-volume-name.tgz
docker run --rm -v "$TARGET_VOLUME:/target" alpine:3.22 \
  sh -c 'test -z "$(ls -A /target)"'
docker run --rm -v "$TARGET_VOLUME:/target" -v "$(dirname "$ARCHIVE"):/backup:ro" \
  alpine:3.22 tar -C /target -xzf "/backup/$(basename "$ARCHIVE")"

Native PowerShell uses Split-Path to produce the read-only archive mount and archive name:

$TargetVolume = 'exact-empty-target-volume-name'
$Archive = 'C:\Users\operator\thothii-backups\2026-08-05\exact-volume-name.tgz'
$ArchiveDir = Split-Path -Parent $Archive
$ArchiveName = Split-Path -Leaf $Archive
docker run --rm -v "${TargetVolume}:/target" alpine:3.22 `
  sh -ceu 'test -z "$(ls -A /target)"'
if ($LASTEXITCODE -ne 0) { throw 'The restore target volume is not empty.' }
docker run --rm -v "${TargetVolume}:/target" -v "${ArchiveDir}:/backup:ro" `
  alpine:3.22 tar -C /target -xzf "/backup/${ArchiveName}"
if ($LASTEXITCODE -ne 0) { throw 'The volume restore failed.' }

Restore all four volumes from the same backup set, restore protected operator files separately, then run update --check-only, start, doctor, registry status/diagnostics, and a known session before normal use. Never merge an archive into a non-empty volume.

Data-preserving uninstall

Run tht stop, retain the installation descriptor at the same absolute path, and make one verified backup set. In Docker Desktop, remove only this installation's stopped core and frontend containers and optional local images; leave its four named volumes. On Linux, use the containers' exact Compose project labels to remove only those stopped containers. Do not prune global Docker data.

Do not run docker compose down --volumes: it deletes the application data this procedure is meant to preserve. Keep the operator directory and protected secrets if you intend to reinstall. Using the same descriptor path preserves the tht project identity and reconnects the same named volumes after rebuilding the source checkout.

Next: workspaces and Pi

Complete local workspace-registry installation, including Git trust, bindings, pull, validation, diagnostics, and registry recovery. Then use Pi management for provider/model configuration, smoke testing, transactional update, and rollback.

The Git-backed workspace registry is always the workspace source of truth. Local DWH, vector, embedding, or LLM processes remain independent services and are never added to the mandatory ThothII core.