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.
+9 -6
View File
@@ -134,10 +134,13 @@ editing state files.
## Direct support access
Advanced support may inspect the bundled executable directly with the installation's exact
validated Compose file set, for example `docker compose exec core pi --version`. This is read-only
diagnosis, not an update mechanism. Do not run package installers, alter Pi files inside the live
container, mount the Docker socket, expose a browser shell, or use a host Pi as a substitute.
Raw Compose access is unsupported: there is no public operator command that safely reconstructs
the installation's hashed project name, project directory, environment file, optional overrides,
and durable current-image selector for ad-hoc Pi execution. Do not approximate those arguments or
delete/edit lifecycle state for support.
Prefer `thothctl pi status`, `pi doctor`, `pi test`, and `pi logs`, because they include the durable
image selector and redact declared secret values. Share only their sanitized output.
Route direct executable/version checks through `thothctl pi status`, and collect diagnostics with
`thothctl pi doctor`, `thothctl pi test`, and `thothctl pi logs`. These commands are
installation-aware and redact declared secret values. Share only their sanitized output. Do not
run package installers, alter Pi files inside the live container, mount the Docker socket, expose
a browser shell, or use a host Pi as a substitute.
+53 -4
View File
@@ -64,8 +64,17 @@ The safest recovery is to reclone into a new directory. First commit wanted work
backup outside both clones. Then clone with conversion disabled, run the verifier, and copy back
only reviewed changes.
If a reviewed working tree must be repaired in place, make a backup or commit all wanted changes
before continuing. Then use Git's repository attributes to stage a renormalization:
If a reviewed working tree must be repaired in place, Git must first normalize the index, export
that exact index to a separate repair directory, verify the exported bytes, and only then copy the
verified tracked files over the worktree. `git add --renormalize .` alone does not change existing
worktree bytes.
> **WARNING — destructive worktree rewrite.** Make a backup outside the clone or commit every
> wanted tracked change before continuing. The copy step below overwrites tracked worktree bytes
> from the staged index export. Stop if the staged diff does not contain exactly the wanted content;
> untracked files are neither exported nor repaired.
From WSL2, Git Bash, macOS, or Linux:
```sh
git status --short
@@ -73,8 +82,48 @@ git config --local core.autocrlf false
git add --renormalize .
git diff --cached --check
git diff --cached
REPAIR_DIR="$(cd .. && pwd -P)/ThothII-lf-repair"
if [[ -e "$REPAIR_DIR" ]]; then
echo "Choose a new empty LF repair directory: $REPAIR_DIR" >&2
exit 1
fi
mkdir -p "$REPAIR_DIR"
REPAIR_PREFIX="$REPAIR_DIR/"
git checkout-index --all --force --prefix="$REPAIR_PREFIX"
bash scripts/verify-line-endings.sh "$REPAIR_DIR"
# WARNING: destructive copy; make a backup or commit wanted changes before this command.
git ls-files -z | while IFS= read -r -d '' path; do
cp "$REPAIR_DIR/$path" "$path"
done
bash scripts/verify-line-endings.sh
```
Review every staged change before committing. The procedure intentionally avoids destructive Git
resets; replacing the clone is easier to audit and much safer for uncommitted work.
Native Windows PowerShell runs the same Git operations and invokes the byte verifier through Git
for Windows:
```powershell
git status --short
git config --local core.autocrlf false
git add --renormalize .
git diff --cached --check
git diff --cached
$RepairDir = Join-Path (Split-Path -Parent (Get-Location).Path) 'ThothII-lf-repair'
if (Test-Path $RepairDir) { throw 'Choose a new empty LF repair directory.' }
New-Item -ItemType Directory -Path $RepairDir | Out-Null
$RepairPrefix = $RepairDir.Replace('\', '/') + '/'
git checkout-index --all --force --prefix=$RepairPrefix
& "C:\Program Files\Git\bin\bash.exe" scripts/verify-line-endings.sh $RepairDir
if ($LASTEXITCODE -ne 0) { throw 'The staged index export does not satisfy the LF policy.' }
# WARNING: destructive copy; make a backup or commit wanted changes before this command.
git ls-files | ForEach-Object {
Copy-Item -LiteralPath (Join-Path $RepairDir $_) -Destination $_ -Force
}
& "C:\Program Files\Git\bin\bash.exe" scripts/verify-line-endings.sh
if ($LASTEXITCODE -ne 0) { throw 'Tracked worktree bytes were not repaired to the LF policy.' }
```
The first verifier proves the exported index bytes before any overwrite; the final verifier
examines the repaired worktree bytes and must also exit `0`. Review the staged diff again before
committing, then remove the separate repair directory only after inspecting it. The procedure
intentionally avoids `git reset --hard`; replacing the clone is easier to audit and much safer for
uncommitted work.