fix: harden local installation guide
This commit is contained in:
+122
-6
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,9 @@ set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
output="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs-test.XXXXXX")"
|
||||
trap 'rm -f "$output"' EXIT HUP INT TERM
|
||||
verifier_functions="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs-functions.XXXXXX")"
|
||||
negative_root="$(mktemp -d "${TMPDIR:-/tmp}/thoth-install-docs-negative.XXXXXX")"
|
||||
trap 'rm -f "$output" "$verifier_functions"; rm -rf "$negative_root"' EXIT HUP INT TERM
|
||||
|
||||
"$root/scripts/verify-workspace-install-docs.sh" --fixtures-only >"$output"
|
||||
|
||||
@@ -17,7 +19,8 @@ for fixture in \
|
||||
"server manual canonical base+override references" \
|
||||
"canonical local base+override fixture" \
|
||||
"canonical server base+override fixture" \
|
||||
"relative secret-source fixture rejected"; do
|
||||
"relative secret-source fixture rejected" \
|
||||
"CRLF recovery rewrites worktree bytes"; do
|
||||
grep -Fqx "$fixture passed" "$output" >/dev/null || {
|
||||
echo "missing fixture verification: $fixture" >&2
|
||||
cat "$output" >&2
|
||||
@@ -48,3 +51,87 @@ if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|c
|
||||
echo "installation manuals still document a bypassed Compose or copied connector override path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load only the verifier's function definitions so each deliberately unsafe guide can be checked
|
||||
# in isolation without invoking Docker-backed Compose fixtures.
|
||||
sed '/^case "\$mode" in/,$d' "$root/scripts/verify-workspace-install-docs.sh" >"$verifier_functions"
|
||||
# shellcheck source=/dev/null
|
||||
source "$verifier_functions"
|
||||
|
||||
negative_failures=0
|
||||
expect_guide_rejected() {
|
||||
local label="$1" validator="$2" source_guide="$3" relative_path="$4"
|
||||
local mutation="$5" expected_error="$6"
|
||||
local fixture_root="$negative_root/${label// /-}"
|
||||
local fixture_output="$fixture_root/output"
|
||||
mkdir -p "$fixture_root/$(dirname "$relative_path")"
|
||||
cp "$source_guide" "$fixture_root/$relative_path"
|
||||
node - "$fixture_root/$relative_path" "$mutation" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const [path, mutation] = process.argv.slice(2);
|
||||
const original = fs.readFileSync(path, "utf8");
|
||||
let changed = original;
|
||||
switch (mutation) {
|
||||
case "durable-selector":
|
||||
changed = original.replaceAll("--source build", "--source stale-build");
|
||||
break;
|
||||
case "dangerous-volumes":
|
||||
changed = original.replace("Do **not** run `docker compose down --volumes`", "Run `docker compose down --volumes`");
|
||||
break;
|
||||
case "incomplete-powershell":
|
||||
changed = original.replaceAll("icacls.exe", "Write-Output");
|
||||
break;
|
||||
case "broken-crlf":
|
||||
changed = original.replaceAll("git checkout-index --all --force --prefix=", "git add --renormalize . # ");
|
||||
break;
|
||||
case "raw-pi":
|
||||
changed += "\n```sh\ndocker compose exec core pi --version\n```\n";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown negative-fixture mutation: ${mutation}`);
|
||||
}
|
||||
if (changed === original) throw new Error(`negative-fixture mutation made no change: ${mutation}`);
|
||||
fs.writeFileSync(path, changed);
|
||||
NODE
|
||||
set +e
|
||||
(root="$fixture_root"; set -e; "$validator") >"$fixture_output" 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
if [[ $status -eq 0 ]]; then
|
||||
echo "negative fixture accepted: $label" >&2
|
||||
cat "$fixture_output" >&2
|
||||
negative_failures=$((negative_failures + 1))
|
||||
elif ! grep -Fq -- "$expected_error" "$fixture_output"; then
|
||||
echo "negative fixture failed for the wrong reason: $label" >&2
|
||||
cat "$fixture_output" >&2
|
||||
negative_failures=$((negative_failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
expect_guide_rejected \
|
||||
"durable selector keeps old core" verify_local_guide \
|
||||
"$root/docs/install/local.md" docs/install/local.md durable-selector \
|
||||
"installation-aware source update lacks structural token: --source build"
|
||||
expect_guide_rejected \
|
||||
"dangerous down volumes instruction" verify_local_guide \
|
||||
"$root/docs/install/local.md" docs/install/local.md dangerous-volumes \
|
||||
"docker compose down --volumes must appear only in an explicit prose prohibition"
|
||||
expect_guide_rejected \
|
||||
"incomplete native PowerShell path" verify_local_guide \
|
||||
"$root/docs/install/local.md" docs/install/local.md incomplete-powershell \
|
||||
"native PowerShell setup lacks structural token: icacls.exe"
|
||||
expect_guide_rejected \
|
||||
"renormalize leaves CRLF worktree bytes" verify_windows_line_endings_guide \
|
||||
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md broken-crlf \
|
||||
"Windows line-ending guide lacks required instruction: git checkout-index --all --force"
|
||||
expect_guide_rejected \
|
||||
"raw non-installation-aware Pi access" verify_pi_management_guide \
|
||||
"$root/docs/install/pi-management.md" docs/install/pi-management.md raw-pi \
|
||||
"raw non-installation-aware Compose Pi access is forbidden"
|
||||
|
||||
if (( negative_failures != 0 )); then
|
||||
echo "$negative_failures unsafe installation-document fixtures were accepted" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "unsafe installation-document fixtures rejected passed"
|
||||
|
||||
@@ -100,6 +100,82 @@ verify_local_guide() {
|
||||
"http://127.0.0.1:8080" \
|
||||
"git pull --ff-only" \
|
||||
"docker compose down --volumes"
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const source = fs.readFileSync(process.argv[2], "utf8");
|
||||
|
||||
function section(name) {
|
||||
const marker = `## ${name}`;
|
||||
const start = source.indexOf(marker);
|
||||
if (start < 0) throw new Error(`missing section: ${name}`);
|
||||
const next = source.indexOf("\n## ", start + marker.length);
|
||||
return source.slice(start, next < 0 ? source.length : next);
|
||||
}
|
||||
|
||||
function blocks(name, language) {
|
||||
const expression = new RegExp("```" + language + "\\n([\\s\\S]*?)```", "g");
|
||||
return [...section(name).matchAll(expression)].map((match) => match[1]);
|
||||
}
|
||||
|
||||
function requireTokens(label, text, tokens) {
|
||||
for (const token of tokens) {
|
||||
if (!text.includes(token)) throw new Error(`${label} lacks structural token: ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
let inCodeFence = false;
|
||||
for (const line of source.split(/\n/)) {
|
||||
if (line.trimStart().startsWith("```")) {
|
||||
inCodeFence = !inCodeFence;
|
||||
continue;
|
||||
}
|
||||
if (!line.includes("docker compose down --volumes")) continue;
|
||||
const normalized = line.toLowerCase().replaceAll("*", "");
|
||||
if (inCodeFence || !/(do not|never)/.test(normalized) || /^\s*(docker|&?\s*docker)/.test(normalized)) {
|
||||
throw new Error("docker compose down --volumes must appear only in an explicit prose prohibition");
|
||||
}
|
||||
}
|
||||
|
||||
const setupPowerShell = blocks("Create the local operator files", "powershell").join("\n");
|
||||
requireTokens("native PowerShell setup", setupPowerShell, [
|
||||
"Copy-Item", "New-Item", "icacls.exe", "/inheritance:r", "/grant:r",
|
||||
"WindowsIdentity", "deploy/env/local.env.example", "thothii-installation.yaml",
|
||||
]);
|
||||
|
||||
const healthPowerShell = blocks("Start and verify", "powershell").join("\n");
|
||||
requireTokens("native PowerShell health", healthPowerShell, [
|
||||
"curl.exe --fail", "http://127.0.0.1:8080/health", "http://127.0.0.1:8787/health",
|
||||
"pi doctor", "pi test",
|
||||
]);
|
||||
|
||||
const updateShell = blocks("Update an installation", "sh").join("\n");
|
||||
requireTokens("installation-aware source update", updateShell, [
|
||||
"NEXT_PI_VERSION", "RUNNING_PI_VERSION", "--source build", "git rev-parse HEAD",
|
||||
"pi status", "status", "doctor", "curl --fail",
|
||||
]);
|
||||
if (!updateShell.includes("NEXT_PI_VERSION\" == \"$RUNNING_PI_VERSION") ||
|
||||
!updateShell.includes("exit 1")) {
|
||||
throw new Error("source update must fail closed when thothctl would no-op on the current Pi version");
|
||||
}
|
||||
|
||||
const updatePowerShell = blocks("Update an installation", "powershell").join("\n");
|
||||
requireTokens("native PowerShell source update", updatePowerShell, [
|
||||
"$NextPiVersion", "$RunningPiVersion", "--source build", "git rev-parse HEAD",
|
||||
"pi status", "status", "doctor", "curl.exe --fail", "throw",
|
||||
]);
|
||||
|
||||
const backupPowerShell = blocks("Back up and restore", "powershell").join("\n");
|
||||
requireTokens("native PowerShell backup/restore", backupPowerShell, [
|
||||
"$BackupDir", "$Volume", "-czf", "$TargetVolume", "$Archive", "Split-Path -Parent",
|
||||
"Split-Path -Leaf", "test -z", "-xzf",
|
||||
]);
|
||||
|
||||
for (const block of [setupPowerShell, healthPowerShell, updatePowerShell, backupPowerShell]) {
|
||||
if (/\$\((dirname|basename)\b|\bmkdir -p\b|\bchmod\s+[0-7]/.test(block)) {
|
||||
throw new Error("native PowerShell block contains a POSIX-only command sequence");
|
||||
}
|
||||
}
|
||||
NODE
|
||||
echo "local installation guide contract passed"
|
||||
}
|
||||
|
||||
@@ -118,22 +194,52 @@ verify_windows_line_endings_guide() {
|
||||
"git config --local core.autocrlf false" \
|
||||
"bash scripts/verify-line-endings.sh" \
|
||||
"git add --renormalize ." \
|
||||
"git checkout-index --all --force" \
|
||||
"git diff --cached --check" \
|
||||
"reclone"
|
||||
if grep -Fq 'git reset --hard' "$guide"; then
|
||||
node - "$guide" <<'NODE'
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const lines = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
|
||||
const commands = [
|
||||
"git add --renormalize .",
|
||||
"git checkout-index --all --force --prefix=",
|
||||
"bash scripts/verify-line-endings.sh",
|
||||
];
|
||||
let prior = -1;
|
||||
for (const command of commands) {
|
||||
const index = lines.findIndex((line, candidate) => candidate > prior && line.trim().includes(command));
|
||||
if (index < 0) throw new Error(`CRLF recovery lacks ordered command: ${command}`);
|
||||
prior = index;
|
||||
}
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
if (!lines[index].includes("git reset --hard")) continue;
|
||||
const command = lines[index].trim();
|
||||
if (command !== 'cp "$REPAIR_DIR/$path" "$path"' &&
|
||||
!command.startsWith("Copy-Item -LiteralPath") &&
|
||||
command !== "git reset --hard") continue;
|
||||
const warning = lines.slice(Math.max(0, index - 4), index).join(" ").toLowerCase();
|
||||
if (!warning.includes("warning") || !warning.includes("destructive") ||
|
||||
!warning.includes("backup") || !warning.includes("commit")) {
|
||||
throw new Error("git reset --hard lacks an immediate destructive warning requiring backup/commit");
|
||||
throw new Error("worktree rewrite lacks an immediate destructive warning requiring backup/commit");
|
||||
}
|
||||
}
|
||||
NODE
|
||||
fi
|
||||
local fixture
|
||||
fixture="$(mktemp -d "${TMPDIR%/}/thoth-crlf-repair.XXXXXX")"
|
||||
trap 'rm -rf "$fixture"' RETURN
|
||||
git -C "$fixture" init -q
|
||||
printf '*.sh text eol=lf\n' >"$fixture/.gitattributes"
|
||||
printf '#!/bin/sh\r\nexit 0\r\n' >"$fixture/repair.sh"
|
||||
git -C "$fixture" add .gitattributes repair.sh 2>/dev/null
|
||||
git -C "$fixture" config --local core.autocrlf false
|
||||
git -C "$fixture" add --renormalize .
|
||||
local export_dir="$fixture-export"
|
||||
mkdir -p "$export_dir"
|
||||
git -C "$fixture" checkout-index --all --force --prefix="$export_dir/"
|
||||
"$root/scripts/verify-line-endings.sh" "$export_dir"
|
||||
cp "$export_dir/repair.sh" "$fixture/repair.sh"
|
||||
"$root/scripts/verify-line-endings.sh" "$fixture"
|
||||
rm -rf "$export_dir"
|
||||
echo "CRLF recovery rewrites worktree bytes passed"
|
||||
echo "Windows line-ending recovery guide contract passed"
|
||||
}
|
||||
|
||||
@@ -163,9 +269,25 @@ verify_pi_management_guide() {
|
||||
"pi maintenance recover --yes" \
|
||||
"pi logs" \
|
||||
"/run/secrets" \
|
||||
"docker compose exec core pi" \
|
||||
"Raw Compose access is unsupported" \
|
||||
"no browser shell" \
|
||||
"does not mount the Docker socket"
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const source = fs.readFileSync(process.argv[2], "utf8");
|
||||
if (/docker\s+compose(?:.|\n){0,160}\bexec\b(?:.|\n){0,80}\bcore\b(?:.|\n){0,80}\bpi\b/i.test(source)) {
|
||||
throw new Error("raw non-installation-aware Compose Pi access is forbidden");
|
||||
}
|
||||
const marker = "## Direct support access";
|
||||
const start = source.indexOf(marker);
|
||||
const support = start < 0 ? "" : source.slice(start, source.indexOf("\n## ", start + marker.length) < 0
|
||||
? source.length : source.indexOf("\n## ", start + marker.length));
|
||||
for (const token of ["unsupported", "thothctl", "pi status", "pi doctor", "pi test", "pi logs"]) {
|
||||
if (!support.toLowerCase().includes(token.toLowerCase())) {
|
||||
throw new Error(`direct support section lacks installation-aware diagnostic: ${token}`);
|
||||
}
|
||||
}
|
||||
NODE
|
||||
echo "Pi management guide contract passed"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user