fix: harden local installation guide

This commit is contained in:
2026-08-05 09:23:44 +02:00
parent 53d257fb76
commit 65aa52115f
5 changed files with 401 additions and 24 deletions
+122 -6
View File
@@ -89,6 +89,25 @@ 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:
```powershell
$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`, external service endpoints, and the absolute
`THT_WORKSPACE_BINDINGS_ENV_FILE`. Create each secret as a separate regular file under the
@@ -206,30 +225,112 @@ curl --fail http://127.0.0.1:8787/health
"$THTCTL" --installation "$INSTALLATION" pi test
```
Native PowerShell must call `curl.exe` explicitly; Windows PowerShell may otherwise resolve `curl`
to `Invoke-WebRequest`:
```powershell
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
& $THTCTL --installation $INSTALLATION pi doctor
& $THTCTL --installation $INSTALLATION pi test
```
Open <http://127.0.0.1:8080>. If a check fails, run `thothctl ... 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. Application source updates are separate from Pi
lifecycle updates:
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. The supported source-update path is a transactional
`pi update --source build` from a clean pulled checkout whose `docker/core.Dockerfile` pins a
different Pi version than the running installation. `thothctl` currently treats the same requested
Pi version as a no-op. The explicit comparison below therefore stops instead of silently deploying
only part of a revision. If it stops, keep the current installation running and wait for a release
with a new Pi pin or a future supported reconciliation command; there is no supported manual
same-version selector-removal procedure.
macOS, Linux, and WSL2:
```sh
"$THTCTL" --installation "$INSTALLATION" stop
git status --short
git diff --quiet
git diff --cached --quiet
git pull --ff-only
git config --local core.autocrlf false
bash scripts/verify-line-endings.sh
SOURCE_REVISION="$(git rev-parse HEAD)"
NEXT_PI_VERSION="$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)"
RUNNING_PI_VERSION="$("$THTCTL" --installation "$INSTALLATION" pi status)"
RUNNING_PI_VERSION="${RUNNING_PI_VERSION#Pi version: }"
if [[ -z "$NEXT_PI_VERSION" || "$NEXT_PI_VERSION" == "$RUNNING_PI_VERSION" ]]; then
echo "Source update stopped: the pulled revision must pin a new Pi version." >&2
exit 1
fi
bash scripts/build-local.sh
bash scripts/build-thothctl.sh
"$THTCTL" --installation "$INSTALLATION" update --check-only
"$THTCTL" --installation "$INSTALLATION" pi update \
--version "$NEXT_PI_VERSION" --source build --yes --drain
"$THTCTL" --installation "$INSTALLATION" start
curl --fail http://127.0.0.1:8080/health
curl --fail http://127.0.0.1:8787/health
printf 'Built source revision: %s\n' "$SOURCE_REVISION"
"$THTCTL" --installation "$INSTALLATION" status
"$THTCTL" --installation "$INSTALLATION" pi status
"$THTCTL" --installation "$INSTALLATION" doctor
```
On native Windows use the PowerShell build launcher and the Git-for-Windows Bash LF check. Review
release notes before updating. Pi has its own transactional `pi update` and rollback workflow in
[Pi management](pi-management.md); never install a package in the running container.
Native Windows PowerShell uses the same fail-closed version comparison and transactional promotion:
```powershell
git status --short
git diff --quiet
if ($LASTEXITCODE -ne 0) { throw 'Commit or back up tracked source changes before update.' }
git diff --cached --quiet
if ($LASTEXITCODE -ne 0) { throw 'Commit or back up staged source changes before update.' }
git pull --ff-only
if ($LASTEXITCODE -ne 0) { throw 'The source pull failed.' }
git config --local core.autocrlf false
& "C:\Program Files\Git\bin\bash.exe" scripts/verify-line-endings.sh
if ($LASTEXITCODE -ne 0) { throw 'The pulled checkout contains CRLF files.' }
$SourceRevision = git rev-parse HEAD
$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
$RunningPiVersion = (& $THTCTL --installation $INSTALLATION pi status) `
-replace '^Pi version:\s*', ''
if ([string]::IsNullOrWhiteSpace($NextPiVersion) -or $NextPiVersion -eq $RunningPiVersion) {
throw 'Source update stopped: the pulled revision must pin a new Pi version.'
}
powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1
if ($LASTEXITCODE -ne 0) { throw 'The local image build failed.' }
& "C:\Program Files\Git\bin\bash.exe" scripts/build-thothctl.sh
if ($LASTEXITCODE -ne 0) { throw 'The thothctl build failed.' }
& $THTCTL --installation $INSTALLATION update --check-only
if ($LASTEXITCODE -ne 0) { throw 'The installation render check failed.' }
& $THTCTL --installation $INSTALLATION pi update `
--version $NextPiVersion --source build --yes --drain
if ($LASTEXITCODE -ne 0) { throw 'The transactional core update failed.' }
& $THTCTL --installation $INSTALLATION start
if ($LASTEXITCODE -ne 0) { throw 'The installation start failed.' }
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
Write-Output "Built source revision: $SourceRevision"
& $THTCTL --installation $INSTALLATION status
& $THTCTL --installation $INSTALLATION pi status
& $THTCTL --installation $INSTALLATION doctor
```
The recorded Git revision identifies the clean worktree used for the candidate build. In
`thothctl status`, confirm that `core` reports the installation lifecycle candidate image, then
require `pi status` to equal the new pin and `doctor` to pass. This is the supported running-image
and source-revision evidence; `update --check-only` alone proves only that Compose renders.
Review release notes before updating. See [Pi management](pi-management.md) for rollback; never
install a package in the running container.
## Back up and restore
@@ -278,6 +379,21 @@ 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:
```powershell
$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.