fix: close deployment decoupling review

This commit is contained in:
2026-08-05 07:52:32 +02:00
parent 5d037e97c4
commit 09834d5cd4
45 changed files with 1082 additions and 791 deletions
+2 -2
View File
@@ -3,11 +3,11 @@ $ErrorActionPreference = "Continue"
$repositoryRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repositoryRoot
& docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
& docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml build --pull
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Output "Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d"
Write-Output "Next: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up -d"
}
exit $exitCode
+3 -2
View File
@@ -3,11 +3,12 @@ set -u
cd "$(dirname "$0")/.."
docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
docker compose --env-file deploy/env/local.env \
-f compose.yaml -f deploy/compose.local.yaml build --pull
status=$?
if [[ "$status" -eq 0 ]]; then
printf '%s\n' 'Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d'
printf '%s\n' 'Next: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up -d'
fi
exit "$status"
+8 -8
View File
@@ -1,21 +1,21 @@
#!/usr/bin/env bash
# Smoke test del deploy standalone ThothII (core + frontend).
# Usa docker-compose.dev.yml (rete propria, porte host).
# Prereq: deploy/thothii.env popolato, endpoint esterni configurati e profilo Pi locale.
# Prereq: deploy/env/local.env popolato, endpoint esterni e file Pi/segreti configurati.
set -euo pipefail
cd "$(dirname "$0")/.."
DC="docker compose -f docker-compose.dev.yml"
DC=(docker compose --env-file deploy/env/local.env -f docker-compose.dev.yml)
WS="/app/harness/workspaces/local.yaml"
echo "== ThothII standalone smoke =="
$DC config --quiet
"${DC[@]}" config --quiet
echo "== Build =="
$DC build
"${DC[@]}" build
echo "== Up (wait health) =="
$DC up -d --wait
"${DC[@]}" up -d --wait
echo "== Core health =="
curl -fsS http://localhost:8787/health && echo
@@ -24,12 +24,12 @@ echo "== Frontend serve =="
curl -fsSI http://localhost:8090/ | head -1
echo "== Wiring check (config + DWH ping; -c è per-command) =="
$DC exec -T core tht config check -c "$WS" || \
"${DC[@]}" exec -T core tht config check -c "$WS" || \
echo "(config check non verde: verificare .env/ruoli DB)"
$DC exec -T core tht db ping -c "$WS" || \
"${DC[@]}" exec -T core tht db ping -c "$WS" || \
echo "(db ping non verde: verificare ruolo thoth_dwh_reader + rete)"
echo "== Down =="
$DC down
"${DC[@]}" down
echo "OK: smoke standalone passato."
@@ -101,7 +101,13 @@ done < <(
{
printf '%s\n' '# Generated by scripts/generate-connector-secrets-override.sh; keep this file untracked.'
printf '%s\n' 'services:' ' core:' ' secrets:'
printf '%s\n' \
'services:' \
' core:' \
' env_file:' \
' - path: ${THT_WORKSPACE_BINDINGS_ENV_FILE:?set THT_WORKSPACE_BINDINGS_ENV_FILE}' \
' required: true' \
' secrets:'
for ((index = 0; index < ${#names[@]}; index += 1)); do
printf ' - source: connector_secret_%d\n' "$((index + 1))"
printf ' target: %s\n' "${targets[index]}"
+8 -1
View File
@@ -39,6 +39,13 @@ write_bundle() {
}
write_bundle
export THT_SECRETS_FILE="$bundle"
printf '%s\n' '{}' >"$secret_dir/pi-auth.json"
chmod 0600 "$secret_dir/pi-auth.json"
operator_env="$secret_dir/operator.env"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$secret_dir/pi-auth.json" \
"THT_SECRETS_FILE=$bundle" >"$operator_env"
# The rotation helper has an old/new file interface; these are test-only
# scratch files and are never mounted into a Compose service.
printf '%s' "$bootstrap_password" >"$secret_dir/bootstrap"
@@ -47,7 +54,7 @@ export THT_VECTOR_BOOTSTRAP_USER=thoth_bootstrap_smoke
export THOTH_SMOKE_OWNER="$smoke_owner"
compose() {
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
docker compose --env-file "$operator_env" -f compose.yaml -f deploy/compose.local-vector.yaml \
--project-name "$smoke_project" --profile local-vector "$@"
}
+8 -1
View File
@@ -58,6 +58,13 @@ bundle="$tmp/thothii.secrets"
chmod 0600 "$bundle"
export THT_SECRETS_FILE="$bundle"
export THT_OLLAMA_URL=http://mock-embeddings:8081
printf '%s\n' '{}' >"$tmp/pi-auth.json"
chmod 0600 "$tmp/pi-auth.json"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$tmp/pi-auth.json" \
"THT_SECRETS_FILE=$bundle" \
'THT_OLLAMA_URL=http://mock-embeddings:8081' >"$tmp/operator.env"
cat >"$tmp/smoke.yaml" <<YAML
services:
@@ -83,7 +90,7 @@ services:
mock-embeddings: {condition: service_started}
YAML
compose="docker compose -f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml -f $tmp/smoke.yaml --project-name $project --profile local-vector --profile preprocess"
compose="docker compose --env-file $tmp/operator.env -f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml -f $tmp/smoke.yaml --project-name $project --profile local-vector --profile preprocess"
$compose build preprocess-evidence
if [ "${PREPROCESS_SMOKE_INJECT_FAILURE:-0}" = "1" ]; then
sh -c 'exit 97'
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Active installation manuals must drive the canonical two-service base+profile stack.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
tmp="$(mktemp -d "${TMPDIR%/}/thoth-canonical-install.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
for retired_example in \
"$root/docs/install/examples/local-compose.workspace-registry.yaml" \
"$root/docs/install/examples/server-compose.workspace-registry.yaml" \
"$root/docs/install/examples/git-ssh.workspace-registry.yaml" \
"$root/docs/install/examples/git-https.workspace-registry.yaml"; do
if [[ -e "$retired_example" ]]; then
echo "superseded one-service install example remains active: ${retired_example#"$root/"}" >&2
exit 1
fi
done
printf '%s\n' '{}' >"$tmp/pi-auth.json"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
mkdir -p "$tmp/data" "$tmp/pi-state" "$tmp/workspace-registry"
printf '%s\n' 'fixture-session-password' >"$tmp/session-runtime-password"
printf '%s\n' 'fixture-session-migrator-password' >"$tmp/session-migrator-password"
printf '%s\n' 'fixture-session-ca' >"$tmp/session-ca.pem"
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$tmp/server-sessions.yaml"
chmod 0600 "$tmp/session-runtime-password" "$tmp/session-migrator-password" "$tmp/session-ca.pem"
for profile in local server; do
env_file="$tmp/$profile.env"
{
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$tmp/pi-auth.json" \
"THT_SECRETS_FILE=$tmp/thothii.secrets"
if [[ "$profile" == server ]]; then
printf '%s\n' \
"THT_DATA_ROOT=$tmp/data" \
"THT_PI_STATE_ROOT=$tmp/pi-state" \
"THT_WORKSPACE_REGISTRY_ROOT=$tmp/workspace-registry" \
"THT_SERVER_WORKSPACE_CONFIG=$tmp/server-sessions.yaml" \
'THT_SESSION_DB_HOST=sessions.example.invalid' \
'THT_SESSION_DB_NAME=thoth_sessions' \
'THT_SESSION_RUNTIME_USER=thoth_sessions_app' \
'THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate' \
"THT_SESSION_RUNTIME_PASSWORD_SOURCE=$tmp/session-runtime-password" \
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$tmp/session-migrator-password" \
"THT_SESSION_CA_SOURCE=$tmp/session-ca.pem"
fi
} >"$env_file"
compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.$profile.yaml")
if [[ "$profile" == server ]]; then
compose_files+=(-f "$root/deploy/compose.session-server.yaml.example")
fi
docker compose --env-file "$env_file" "${compose_files[@]}" \
config --format json >"$tmp/$profile.json"
node - "$tmp/$profile.json" "$profile" <<'NODE'
const fs = require("fs");
const [path, profile] = process.argv.slice(2);
const config = JSON.parse(fs.readFileSync(path, "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
throw new Error(profile + ": install stack must be exactly core,frontend");
}
if (!config.services.core.secrets?.some((secret) => secret.target === "thothii.secrets")) {
throw new Error(profile + ": install stack lacks the runtime secret bundle");
}
if (!config.services.core.volumes?.some(
(mount) => mount.target === "/home/thoth/.pi/agent/auth.json" && mount.read_only,
)) {
throw new Error(profile + ": install stack lacks the read-only Pi auth file");
}
if ((config.services.frontend.secrets || []).length !== 0) {
throw new Error(profile + ": frontend received runtime secrets");
}
if (profile === "server" && config.services.core.environment?.THT_SESSION_STORAGE !== "postgres") {
throw new Error("server: public startup must include the PostgreSQL session override");
}
if (JSON.stringify(config).includes("fixture-model-api-key")) {
throw new Error(profile + ": rendered Compose leaked a secret value");
}
NODE
done
for profile in local server; do
manual="$root/docs/install/$profile-workspace-registry.md"
grep -Fq -- '--env-file "$THT_OPERATOR_ENV"' "$manual" \
&& grep -Fq -- "-f \"\$THT_SOURCE_ROOT/compose.yaml\" -f \"\$THT_SOURCE_ROOT/deploy/compose.$profile.yaml\"" "$manual" || {
echo "$profile manual lacks the canonical base+profile command" >&2
exit 1
}
if rg -q 'local-compose\.workspace-registry|server-compose\.workspace-registry' "$manual"; then
echo "$profile manual still references a superseded standalone Compose example" >&2
exit 1
fi
done
echo "canonical install Compose contract passed."
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Fresh Compose flow: mounted Pi policy/auth must produce a selectable, credential-ready provider.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
tmp="$(mktemp -d "${TMPDIR%/}/thoth-provider-readiness.XXXXXX")"
project="thothii-provider-readiness-$$"
compose=(
docker compose --project-name "$project" --env-file "$tmp/local.env"
-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml"
)
cleanup() {
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
rm -rf "$tmp"
}
trap cleanup EXIT HUP INT TERM
printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tmp/pi-auth.json"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$tmp/pi-auth.json" \
"THT_SECRETS_FILE=$tmp/thothii.secrets" \
'THOTH_CORE_HTTP_PORT=0' \
'THOTH_HTTP_PORT=0' \
>"$tmp/local.env"
"${compose[@]}" up --detach --wait --wait-timeout 90 --build core
core_id="$("${compose[@]}" ps -q core)"
core_address="$("${compose[@]}" port core 8787 | head -n 1)"
"${compose[@]}" exec -T core sh -ceu '
test -r /home/thoth/.pi/agent/auth.json
test -r /home/thoth/.pi/agent/models.json
test -r /home/thoth/.pi/agent/settings.json
test -r /run/secrets/thothii.secrets
'
curl --fail --silent --show-error "http://$core_address/models" >"$tmp/models.json"
node - "$tmp/models.json" <<'NODE'
const fs = require("fs");
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (!body.models?.some((model) => model.provider === "zai" && model.id === "glm-5.2")) {
throw new Error("fresh Compose did not expose the mounted Pi-enabled model");
}
NODE
curl --fail --silent --show-error -X PUT \
-H 'content-type: application/json' \
--data '{"provider":"zai","model":"glm-5.2","reasoning":"low"}' \
"http://$core_address/pi-management/config" >"$tmp/configured.json"
curl --fail --silent --show-error \
"http://$core_address/pi-management/status" >"$tmp/status.json"
node - "$tmp/status.json" <<'NODE'
const fs = require("fs");
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (!body.ready || body.credentials !== "present") {
throw new Error("mounted Pi provider is not credential-ready");
}
if (body.config?.provider !== "zai" || body.config?.model !== "glm-5.2") {
throw new Error("Pi provider configuration was not persisted");
}
NODE
inspect="$(docker inspect "$core_id")"
for secret in fixture-native-auth-key fixture-model-api-key; do
if grep -Fq "$secret" <<<"$inspect"; then
echo "container inspection leaked $secret" >&2
exit 1
fi
done
echo "Compose provider-readiness contract passed."
+15 -6
View File
@@ -48,7 +48,13 @@ for (const mount of (core.volumes || []).filter((item) => item.target?.startsWit
const secretTargets = (core.secrets || []).map((secret) => secret.target).sort();
const expectedSecrets = expectedSecretTargets ? expectedSecretTargets.split(",").filter(Boolean).sort() : [];
if (secretTargets.join(",") !== expectedSecrets.join(",")) {
throw new Error(`${name}: connector targets do not match generated THT_WS_*_FILE bindings`);
throw new Error(`${name}: Docker secret targets do not match the deployment contract`);
}
if (core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
throw new Error(`${name}: core does not use the canonical /run/secrets bundle path`);
}
if ((config.services.frontend?.secrets || []).length !== 0) {
throw new Error(`${name}: frontend must not receive runtime secrets`);
}
if (name === "ssh") {
@@ -62,7 +68,7 @@ if (name === "https" && core.environment?.GIT_CONFIG_VALUE_1 !== "/run/secrets/w
}
const rendered = JSON.stringify(config);
for (const secret of ["fixture-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-api-key"]) {
for (const secret of ["fixture-model-api-key", "fixture-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-api-key"]) {
if (rendered.includes(secret)) throw new Error(`${name}: rendered Compose leaked fixture secret value`);
}
NODE
@@ -97,6 +103,7 @@ assert_unsafe_source_rejected() {
}
write_secret "$fixture_root/pi-auth.json" 'fixture-pi-auth'
write_secret "$fixture_root/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
write_secret "$fixture_root/ssh-private-key" 'fixture-ssh-private-key'
write_secret "$fixture_root/ssh-known-hosts" 'fixture-ssh-known-hosts'
write_secret "$fixture_root/https-credentials" 'fixture-https-credentials'
@@ -107,6 +114,8 @@ write_secret "$fixture_root/vector-api-key" 'fixture-vector-api-key'
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture_root/workspace-bindings.env" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture_root/ssh-private-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture_root/ssh-known-hosts" \
"THT_WORKSPACE_GIT_CREDENTIALS_FILE=$fixture_root/https-credentials" \
@@ -127,13 +136,13 @@ connector_override="$fixture_root/compose.connector-secrets.local.yaml"
--output "$connector_override"
render base
assert_render_contract base '' ''
assert_render_contract base '' 'thothii.secrets'
render ssh -f "$root/deploy/compose.git-ssh.yaml"
assert_render_contract ssh '/run/secrets/workspace-registry-git-known-hosts,/run/secrets/workspace-registry-git-ssh-key' ''
assert_render_contract ssh '/run/secrets/workspace-registry-git-known-hosts,/run/secrets/workspace-registry-git-ssh-key' 'thothii.secrets'
render https -f "$root/deploy/compose.git-https.yaml"
assert_render_contract https '/run/secrets/workspace-registry-git-ca,/run/secrets/workspace-registry-git-credentials' ''
assert_render_contract https '/run/secrets/workspace-registry-git-ca,/run/secrets/workspace-registry-git-credentials' 'thothii.secrets'
render connector -f "$connector_override"
assert_render_contract connector '' 'north-star-research-dwh-password,north-star-research-vector-api-key'
assert_render_contract connector '' 'north-star-research-dwh-password,north-star-research-vector-api-key,thothii.secrets'
assert_missing_source_rejected THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE
assert_missing_source_rejected THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE
+8 -1
View File
@@ -9,10 +9,13 @@ expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
trap 'docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT HUP INT TERM
test -n "$expected_pi_version"
printf '{}\n' >"$tmp/pi-auth.json"
printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tmp/pi-auth.json"
chmod 0600 "$tmp/pi-auth.json"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/thothii.secrets"
export PI_AUTH_FILE="$tmp/pi-auth.json"
export THT_SECRETS_FILE="$tmp/thothii.secrets"
export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces.git"
# Let Docker assign loopback ports so this isolated contract test never collides with an operator stack.
export THOTH_CORE_HTTP_PORT=0
@@ -62,6 +65,10 @@ docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local
test "$(pi --version)" = "$PI_VERSION"
command -v pi >/dev/null
test ! -e /var/run/docker.sock
test -r /home/thoth/.pi/agent/auth.json
test -r /home/thoth/.pi/agent/models.json
test -r /home/thoth/.pi/agent/settings.json
test -r /run/secrets/thothii.secrets
touch /data/.task5-writable
rm /data/.task5-writable
if find /app /home /data -xdev \( -iname "*chirone*" -o -iname "*omics*portal*" \) -print -quit | grep -q .; then
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Prevent active operator-facing startup examples from bypassing required env/profile inputs.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
cd "$root"
targets=(
README.md
.env.example
docker-compose.dev.yml
deploy/env.example
deploy/secrets/README.md
docs/install
docs/index.md
docs/installazione-docker-4-contesti.md
scripts/build-local.sh
scripts/build-local.ps1
scripts/docker-smoke.sh
scripts/local-vector-smoke.sh
scripts/preprocess-smoke.sh
scripts/vector-rotate-bootstrap-password.sh
)
existing=()
for target in "${targets[@]}"; do
[[ ! -e "$target" ]] || existing+=("$target")
done
set +e
matches="$(rg -n \
'docker compose (up|build|run|config|ps|exec|-f)|DC="docker compose -f|compose="docker compose -f' \
"${existing[@]}" 2>&1)"
rg_status=$?
set -e
case "$rg_status" in
0)
echo "active deployment command omits --env-file before its action/overrides:" >&2
printf '%s\n' "$matches" >&2
exit 1
;;
1) ;;
*)
printf '%s\n' "$matches" >&2
exit "$rg_status"
;;
esac
for document in README.md docs/installazione-docker-4-contesti.md; do
grep -Fq -- '-f deploy/compose.session-server.yaml.example' "$document" || {
echo "$document omits the required public-server session override" >&2
exit 1
}
done
for required in \
THT_SERVER_WORKSPACE_CONFIG \
THT_SESSION_RUNTIME_PASSWORD_SOURCE \
THT_SESSION_MIGRATOR_PASSWORD_SOURCE \
THT_SESSION_CA_SOURCE; do
grep -q "^$required=" deploy/env/server.env.example || {
echo "server env example omits $required" >&2
exit 1
}
done
echo "deployment command contract passed."
@@ -8,6 +8,7 @@ trap cleanup EXIT HUP INT TERM
export THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git
export PI_AUTH_FILE=/dev/null
export THT_SECRETS_FILE=/dev/null
unset THT_VECTOR_BOOTSTRAP_PASSWORD_SECRET_FILE THT_VECTOR_MIGRATOR_PASSWORD_SECRET_FILE
unset THT_VECTOR_READER_PASSWORD_SECRET_FILE THT_VECTOR_WRITER_PASSWORD_SECRET_FILE
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Model providers are external endpoints reached through the ordinary application network.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
tmp="$(mktemp -d "${TMPDIR%/}/thoth-external-llm.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
printf '%s\n' '{}' >"$tmp/pi-auth.json"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
PI_AUTH_FILE="$tmp/pi-auth.json" \
THT_SECRETS_FILE="$tmp/thothii.secrets" \
THT_LLM_URL=https://llm.example.invalid/v1 \
docker compose -f "$root/compose.yaml" config --format json >"$tmp/config.json"
node - "$tmp/config.json" <<'NODE'
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
throw new Error("external LLM deployment must retain the mandatory two-service stack");
}
if (Object.keys(config.networks || {}).join(",") !== "thothii") {
throw new Error("external LLM endpoint must not require a provider-owned Docker network");
}
if (config.services.core.environment?.THT_LLM_URL !== "https://llm.example.invalid/v1") {
throw new Error("core did not receive the generic external LLM endpoint");
}
const joins = (service, network) => Array.isArray(service.networks)
? service.networks.includes(network)
: Object.hasOwn(service.networks || {}, network);
if (!joins(config.services.core, "thothii") || !joins(config.services.frontend, "thothii")) {
throw new Error("frontend and core must share only the application network");
}
NODE
echo "external LLM network contract passed."
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Regression coverage for coupling-scan categories, exact exclusions, and scanner failures.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
fixture="$(mktemp -d "${TMPDIR%/}/thoth-coupling-scope.XXXXXX")"
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
new_fixture() {
rm -rf "$fixture/repository"
mkdir -p \
"$fixture/repository/deploy/env" \
"$fixture/repository/deploy/workspaces" \
"$fixture/repository/docker/smoke" \
"$fixture/repository/docs/install" \
"$fixture/repository/docs/superpowers/plans" \
"$fixture/repository/frontend" \
"$fixture/repository/scripts"
printf '%s\n' 'services: {}' >"$fixture/repository/compose.yaml"
printf '%s\n' '# generic runtime image' >"$fixture/repository/docker/core.Dockerfile"
printf '%s\n' '# generic smoke' >"$fixture/repository/docker/smoke/core-smoke.sh"
printf '%s\n' '# generic install' >"$fixture/repository/docs/install/local.md"
printf '%s\n' 'THT_LLM_URL=https://llm.example.invalid' >"$fixture/repository/deploy/env/local.env.example"
printf '%s\n' '# generic launcher' >"$fixture/repository/scripts/run-stack.sh"
printf '%s\n' '// generic frontend configuration' >"$fixture/repository/frontend/vite.config.ts"
# These are the three intentionally allowed categories from the Task 10 boundary.
printf '%s\n' 'historical omics_portal and Chirone record' \
>"$fixture/repository/docs/superpowers/plans/legacy.md"
printf '%s\n' 'id: psd' >"$fixture/repository/deploy/workspaces/psd.yaml.example"
printf '%s\n' '# migrate PSD sessions from /home/chirone' \
>"$fixture/repository/docker/session-migrate.sh"
}
assert_clean() {
"$root/scripts/test-no-deployment-coupling.sh" --root "$fixture/repository" >/dev/null
}
assert_detected() {
local relative_path="$1" content="$2" output status
new_fixture
mkdir -p "$(dirname "$fixture/repository/$relative_path")"
printf '%s\n' "$content" >"$fixture/repository/$relative_path"
set +e
output="$("$root/scripts/test-no-deployment-coupling.sh" --root "$fixture/repository" 2>&1)"
status=$?
set -e
if [[ $status -ne 1 ]] || ! grep -Fq "$relative_path" <<<"$output"; then
echo "coupling scan missed $relative_path" >&2
printf '%s\n' "$output" >&2
exit 1
fi
}
new_fixture
assert_clean
assert_detected compose.yaml 'services: # Chirone runtime coupling'
assert_detected docker/smoke/core-smoke.sh 'test -d /home/chirone'
assert_detected docs/install/local.md 'Install the PSD deployment profile.'
assert_detected deploy/env/local.env.example 'NETWORK=omics_portal'
assert_detected scripts/run-stack.sh 'exec datamart-builder'
assert_detected frontend/vite.config.ts 'const base = "/omics_portal";'
assert_detected scripts/test-qwen-network-config.sh 'require localllm_default'
assert_detected scripts/test-provider-network.sh 'if (!config.networks?.localllm_default?.external) exit 1'
assert_detected deploy/compose.psd-local.yaml 'services: {}'
new_fixture
mkdir -p "$fixture/bin"
printf '%s\n' '#!/bin/sh' 'exit 2' >"$fixture/bin/rg"
chmod +x "$fixture/bin/rg"
set +e
PATH="$fixture/bin:$PATH" "$root/scripts/test-no-deployment-coupling.sh" \
--root "$fixture/repository" >"$fixture/rg.out" 2>"$fixture/rg.err"
status=$?
set -e
if [[ $status -ne 2 ]]; then
echo "coupling scan masked an rg failure (status $status)" >&2
cat "$fixture/rg.out" "$fixture/rg.err" >&2
exit 1
fi
echo "no-coupling scope regression tests passed."
+107 -51
View File
@@ -1,69 +1,125 @@
#!/usr/bin/env bash
# Category-based guard for active build, runtime, install, and launch coupling.
set -euo pipefail
cd "$(dirname "$0")/.."
script_root="$(cd "$(dirname "$0")/.." && pwd -P)"
scan_root="$script_root"
if [[ "${1:-}" == --root ]]; then
[[ $# -eq 2 ]] || { echo "usage: $0 [--root PATH]" >&2; exit 2; }
scan_root="$2"
elif [[ $# -ne 0 ]]; then
echo "usage: $0 [--root PATH]" >&2
exit 2
fi
[[ -d "$scan_root" ]] || { echo "coupling scan root is not a directory: $scan_root" >&2; exit 2; }
cd "$scan_root"
content_targets=(
.dockerignore
compose.yaml
docker-compose.dev.yml
deploy
docker
frontend/vite.config.ts
README.md
docs/install
docs/installazione-docker-4-contesti.md
.env.example
scripts/run-stack.sh
scripts/docker-smoke.sh
)
runtime_files=()
install_files=()
operator_files=()
contract_test_files=()
add_file() {
local array_name="$1" file="$2"
[[ ! -f "$file" ]] || eval "$array_name+=(\"\$file\")"
}
matches=$(
rg -n -i \
-g '!deploy/workspaces/**' \
-g '!docker/session-migrate.sh' \
-g '!docker/cutover-legacy-sessions.sh' \
-g '!docker/smoke/**' \
'omics_portal|chirone|localllm_default|datamart-builder|compose\.production\.yaml|compose\.psd-local\.yaml' \
"${content_targets[@]}" || true
)
for file in .dockerignore compose.yaml docker-compose.dev.yml frontend/vite.config.ts; do
add_file runtime_files "$file"
done
if [[ -d deploy ]]; then
while IFS= read -r -d '' file; do runtime_files+=("${file#./}"); done < <(
find deploy -type f ! -path 'deploy/workspaces/*' -print0
)
fi
if [[ -d docker ]]; then
while IFS= read -r -d '' file; do
case "$file" in
docker/session-migrate.sh|docker/cutover-legacy-sessions.sh) continue ;;
esac
runtime_files+=("${file#./}")
done < <(find docker -type f -print0)
fi
runtime_psd_matches=$(
rg -n -i \
-g '!deploy/workspaces/**' \
-g '!docker/session-migrate.sh' \
-g '!docker/cutover-legacy-sessions.sh' \
-g '!docker/smoke/**' \
'\bpsd\b' \
.dockerignore compose.yaml docker-compose.dev.yml deploy docker frontend/vite.config.ts \
.env.example scripts/run-stack.sh scripts/docker-smoke.sh || true
)
for file in README.md .env.example docs/installazione-docker-4-contesti.md; do
add_file install_files "$file"
done
if [[ -d docs/install ]]; then
while IFS= read -r -d '' file; do install_files+=("${file#./}"); done < <(
find docs/install -type f -print0
)
fi
if [[ -d scripts ]]; then
while IFS= read -r -d '' file; do
case "${file#scripts/}" in
test-no-deployment-coupling.sh|test-no-deployment-coupling-scope.sh) continue ;;
test-*.sh)
contract_test_files+=("${file#./}")
continue
;;
verify-*.sh) continue ;;
esac
operator_files+=("${file#./}")
done < <(find scripts -maxdepth 1 -type f -print0)
fi
offenders=()
for superseded_file in \
scan_category() {
local label="$1" pattern="$2"; shift 2
local output rg_status
(($#)) || return 0
set +e
output="$(rg -n -i --with-filename -- "$pattern" "$@" 2>&1)"
rg_status=$?
set -e
case "$rg_status" in
0)
while IFS= read -r match; do offenders+=("$label: $match"); done <<<"$output"
;;
1) ;;
*)
echo "coupling scan failed in $label (rg status $rg_status)" >&2
printf '%s\n' "$output" >&2
exit "$rg_status"
;;
esac
}
for forbidden_file in \
deploy/compose.production.yaml \
deploy/compose.psd-local.yaml.example \
deploy/compose.psd-local.yaml \
scripts/bootstrap-local-psd-docker-config.sh \
harness/tests/test_psd_local_compose_contract.py
do
[[ ! -e "$superseded_file" ]] || offenders+=("$superseded_file (forbidden active deployment filename)")
scripts/test-qwen-network-config.sh \
harness/tests/test_psd_local_compose_contract.py; do
[[ ! -e "$forbidden_file" ]] \
|| offenders+=("active filename: $forbidden_file (superseded deployment contract)")
done
if [[ -n "$matches" ]]; then
while IFS= read -r match; do
offenders+=("$match")
done <<<"$matches"
fi
forbidden='omics_portal|chirone|localllm_default|datamart-builder|compose\.production\.yaml|compose\.psd-local\.yaml|\bpsd\b'
scan_category runtime "$forbidden" "${runtime_files[@]}"
scan_category install "$forbidden" "${install_files[@]}"
scan_category operator "$forbidden" "${operator_files[@]}"
# Contract tests legitimately quote forbidden names in negative assertions. Scan their positive
# deployment wiring constructs instead, so a provider-owned network or retired overlay cannot be
# required under a different test filename.
positive_contract='networks(\?|\.)?\.?localllm_default|services(\?|\.)?\.?core(\?|\.)?\.?networks(\?|\.)?\.?localllm_default|docker compose[^\n]*(compose\.psd-local|compose\.production)|THT_PSD_[A-Z0-9_]*='
scan_category contract-test "$positive_contract" "${contract_test_files[@]}"
if [[ -n "$runtime_psd_matches" ]]; then
while IFS= read -r match; do
offenders+=("$match")
done <<<"$runtime_psd_matches"
fi
if rg -n 'command -v pi|PI_BIN="pi"|PI_BIN=pi' scripts/run-stack.sh >/dev/null; then
offenders+=("scripts/run-stack.sh (requires a host Pi binary)")
if [[ -f scripts/run-stack.sh ]]; then
set +e
host_pi="$(rg -n 'command -v pi|PI_BIN="pi"|PI_BIN=pi' scripts/run-stack.sh 2>&1)"
host_pi_status=$?
set -e
case "$host_pi_status" in
0) offenders+=("operator: $host_pi") ;;
1) ;;
*)
echo "coupling scan failed in host-Pi contract (rg status $host_pi_status)" >&2
printf '%s\n' "$host_pi" >&2
exit "$host_pi_status"
;;
esac
fi
if ((${#offenders[@]})); then
+32 -1
View File
@@ -8,13 +8,42 @@ trap 'rm -rf "$tmp"' EXIT HUP INT TERM
auth_file="$tmp/auth.json"
printf '%s\n' '{}' >"$auth_file"
chmod 0600 "$auth_file"
secrets_file="$tmp/thothii.secrets"
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$secrets_file"
chmod 0600 "$secrets_file"
rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
PI_AUTH_FILE="$auth_file" docker compose config)
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" docker compose config)
printf '%s\n' "$rendered" | grep -q "source: $auth_file"
printf '%s\n' "$rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
printf '%s\n' "$rendered" | grep -A4 'target: /home/thoth/.pi/agent/auth.json' \
| grep -q 'read_only: true'
for target in \
/home/thoth/.pi/agent/models.json \
/home/thoth/.pi/agent/settings.json; do
printf '%s\n' "$rendered" | grep -q "target: $target"
printf '%s\n' "$rendered" | grep -A4 "target: $target" | grep -q 'read_only: true'
done
printf '%s\n' "$rendered" | grep -q "file: $secrets_file"
printf '%s\n' "$rendered" | grep -q 'target: thothii.secrets'
if grep -Fq 'fixture-model-api-key' <<<"$rendered"; then
echo "rendered base Compose leaked the model key" >&2
exit 1
fi
dev_rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" \
docker compose --env-file deploy/env/local.env.example -f docker-compose.dev.yml config)
printf '%s\n' "$dev_rendered" | grep -q "source: $auth_file"
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/models.json'
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/settings.json'
printf '%s\n' "$dev_rendered" | grep -q "file: $secrets_file"
printf '%s\n' "$dev_rendered" | grep -q 'target: thothii.secrets'
if grep -Fq 'fixture-model-api-key' <<<"$dev_rendered"; then
echo "rendered development Compose leaked the model key" >&2
exit 1
fi
python3 - <<'PY'
import json
@@ -32,5 +61,7 @@ PY
grep -q '^ARG PI_VERSION=0.80.3$' docker/core.Dockerfile
grep -q '^PI_AUTH_FILE=/absolute/path/to/pi-auth.json$' deploy/env/local.env.example
grep -q '^PI_AUTH_FILE=/absolute/path/to/pi-auth.json$' deploy/env/server.env.example
grep -q '^THT_SECRETS_FILE=/absolute/path/to/thothii.secrets$' deploy/env/local.env.example
grep -q '^THT_SECRETS_FILE=/absolute/path/to/thothii.secrets$' deploy/env/server.env.example
echo "Pi user-auth Compose contract passed."
+6 -1
View File
@@ -4,7 +4,8 @@ set -eu
cd "$(dirname "$0")/.."
tmp_bundle=$(mktemp)
trap 'rm -f "$tmp_bundle"' EXIT HUP INT TERM
tmp_auth=$(mktemp)
trap 'rm -f "$tmp_bundle" "$tmp_auth"' EXIT HUP INT TERM
cat >"$tmp_bundle" <<'EOF'
THT_VECTOR_BOOTSTRAP_PASSWORD=test-bootstrap
THT_VECTOR_MIGRATOR_PASSWORD=test-migrator
@@ -12,7 +13,11 @@ THT_VECTOR_READER_PASSWORD=test-reader
THT_VECTOR_WRITER_PASSWORD=test-writer
EOF
chmod 0600 "$tmp_bundle"
printf '%s\n' '{}' >"$tmp_auth"
chmod 0600 "$tmp_auth"
export THT_SECRETS_FILE="$tmp_bundle"
export PI_AUTH_FILE="$tmp_auth"
export THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git
local_files="-f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml"
local_json=$(docker compose $local_files --profile local-vector --profile preprocess config --format json)
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir -p "$tmp/deploy"
cp compose.yaml "$tmp/compose.yaml"
: >"$tmp/deploy/thothii.env"
docker compose --project-directory "$tmp" -f "$tmp/compose.yaml" config --format json >"$tmp/config.json"
node - "$tmp/config.json" <<'NODE'
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (!config.networks?.localllm_default?.external) {
throw new Error("localllm_default must be an external network");
}
if (!config.services?.core?.networks?.localllm_default) {
throw new Error("core must join localllm_default");
}
if (config.services?.frontend?.networks?.localllm_default) {
throw new Error("frontend must not join the model network");
}
NODE
+44 -2
View File
@@ -11,8 +11,12 @@ render_profile() {
local env_file=$2
local compose_file=$3
local rendered="$tmp/$profile.json"
local -a files=(-f compose.yaml -f "$compose_file")
if [[ "$profile" == server ]]; then
files+=(-f deploy/compose.session-server.yaml.example)
fi
docker compose --env-file "$env_file" -f compose.yaml -f "$compose_file" \
docker compose --env-file "$env_file" "${files[@]}" \
config --format json >"$rendered"
node - "$rendered" "$profile" <<'NODE'
@@ -38,6 +42,28 @@ const piAuthMounts = (config.services.core.volumes || []).filter(
if (piAuthMounts.length !== 1 || piAuthMounts[0].type !== "bind" || !piAuthMounts[0].read_only) {
throw new Error("Pi auth must be one read-only file bind");
}
if (config.services.core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
throw new Error("core must read the canonical runtime secret bundle from /run/secrets");
}
const runtimeSecrets = config.services.core.secrets || [];
const bundleSecrets = runtimeSecrets.filter(
(secret) => secret.source === "thothii_secrets" && secret.target === "thothii.secrets",
);
if (bundleSecrets.length !== 1) {
throw new Error("core must receive exactly one canonical runtime secret bundle");
}
if (profile === "local" && runtimeSecrets.length !== 1) {
throw new Error("local core must receive only the canonical runtime secret bundle");
}
if (profile === "server") {
const targets = new Set(runtimeSecrets.map((secret) => secret.target));
for (const target of ["session_runtime_password", "session_ca.pem"]) {
if (!targets.has(target)) throw new Error("server core lacks " + target);
}
}
if ((config.services.frontend.secrets || []).length !== 0) {
throw new Error("frontend must not receive runtime secrets");
}
const ports = Object.fromEntries(
Object.entries(config.services).map(([name, service]) => [name, service.ports || []]),
@@ -60,9 +86,12 @@ assert_remote_required() {
local env_file=$1
local compose_file=$2
local without_remote="$tmp/without-remote.env"
local -a files=(-f compose.yaml -f "$compose_file")
[[ "$compose_file" != deploy/compose.server.yaml ]] \
|| files+=(-f deploy/compose.session-server.yaml.example)
grep -v '^THT_WORKSPACE_GIT_REMOTE=' "$env_file" >"$without_remote"
if env -u THT_WORKSPACE_GIT_REMOTE docker compose --env-file "$without_remote" -f compose.yaml -f "$compose_file" \
if env -u THT_WORKSPACE_GIT_REMOTE docker compose --env-file "$without_remote" "${files[@]}" \
config --format json >"$tmp/missing-remote.out" 2>"$tmp/missing-remote.err"; then
echo "Compose must require THT_WORKSPACE_GIT_REMOTE" >&2
exit 1
@@ -72,6 +101,7 @@ assert_remote_required() {
THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
PI_AUTH_FILE=/dev/null \
THT_SECRETS_FILE=/dev/null \
docker compose -f compose.yaml config --format json >"$tmp/base.json"
node - "$tmp/base.json" <<'NODE'
const fs = require("fs");
@@ -98,6 +128,18 @@ const piAuthMounts = (config.services.core.volumes || []).filter(
if (piAuthMounts.length !== 1 || piAuthMounts[0].type !== "bind" || !piAuthMounts[0].read_only) {
throw new Error("Pi auth must be one read-only file bind");
}
if (config.services.core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
throw new Error("core must read the canonical runtime secret bundle from /run/secrets");
}
const runtimeSecrets = config.services.core.secrets || [];
if (runtimeSecrets.length !== 1
|| runtimeSecrets[0].source !== "thothii_secrets"
|| runtimeSecrets[0].target !== "thothii.secrets") {
throw new Error("core must receive exactly the canonical runtime secret bundle");
}
if ((config.services.frontend.secrets || []).length !== 0) {
throw new Error("frontend must not receive runtime secrets");
}
NODE
render_profile local deploy/env/local.env.example deploy/compose.local.yaml
+7 -16
View File
@@ -9,20 +9,11 @@ trap 'rm -f "$output"' EXIT HUP INT TERM
"$root/scripts/verify-workspace-install-docs.sh" --fixtures-only >"$output"
for fixture in \
"local manual requires generated connector override and Compose preflight" \
"server manual requires generated connector override and Compose preflight" \
"local documented shell environment fixture" \
"server documented shell environment fixture" \
"copied local base fixture" \
"copied server PostgreSQL/TLS fixture" \
"copied HTTPS Git override fixture" \
"copied SSH Git override fixture" \
"copied connector binding/secret fixture" \
"core process sees connector bindings and secret files" \
"non-path secret-file fixture rejected" \
"literal secret-source fixture rejected" \
"relative secret-source fixture rejected" \
"non-normalized secret-source fixture rejected"; do
"local manual canonical base+override references" \
"server manual canonical base+override references" \
"canonical local base+override fixture" \
"canonical server base+override fixture" \
"relative secret-source fixture rejected"; do
grep -Fqx "$fixture passed" "$output" >/dev/null || {
echo "missing fixture verification: $fixture" >&2
cat "$output" >&2
@@ -37,7 +28,7 @@ for manual in \
echo "installation manual does not publish a self-contained THT_SOURCE_ROOT export: $manual" >&2
exit 1
}
grep -Fq 'export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"' "$manual" || {
grep -Fq 'export THT_WORKSPACE_BINDINGS_ENV_FILE=' "$manual" || {
echo "installation manual does not publish a self-contained bindings export: $manual" >&2
exit 1
}
@@ -47,7 +38,7 @@ for manual in \
fi
done
if rg -n 'connector-secrets\.workspace-registry|docker compose' \
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' \
"$root/docs/install/local-workspace-registry.md" \
"$root/docs/install/server-workspace-registry.md"; then
echo "installation manuals still document a bypassed Compose or copied connector override path" >&2
+2 -2
View File
@@ -29,7 +29,7 @@ trap 'rm -f "$replacement"' EXIT HUP INT TERM
cp "$new_secret" "$replacement"
chmod 0600 "$replacement"
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local-vector.yaml \
--project-name "$project" --profile local-vector run --rm --no-deps \
--user 0:0 \
--entrypoint /opt/venv/bin/python \
@@ -43,4 +43,4 @@ mv -f "$replacement" "$old_secret"
trap - EXIT HUP INT TERM
echo "Deployment bootstrap secret atomically replaced only after verified database login."
echo "Re-run: docker compose -f compose.yaml -f deploy/compose.local-vector.yaml --project-name $project --profile local-vector up --wait vector-reconcile vector-migrate core"
echo "Re-run: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local-vector.yaml --project-name $project --profile local-vector up --wait vector-reconcile vector-migrate core"
+186 -306
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# Validate the installation manuals without reading an operator environment or production remote.
# Verify canonical local/server installation manuals and their base+override Compose paths.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
profile="${1:-}"
mode="${1:-}"
trim() {
local value="$1"
@@ -41,266 +41,13 @@ verify_path_variable_values() {
fi
fi
done <"$source"
return 0
}
verify_server_public_contract() {
local server_example="$root/docs/install/examples/server-compose.workspace-registry.yaml"
for expected in \
'THT_SESSION_STORAGE: postgres' \
'THT_SESSION_RUNTIME_PASSWORD_FILE: /run/secrets/session_runtime_password' \
'THT_SESSION_DB_SSLMODE: ${THT_SESSION_DB_SSLMODE:-verify-full}' \
'THT_SESSION_DB_SSLROOTCERT: /run/secrets/session_ca.pem' \
'session_runtime_password:' \
'session_ca:'; do
grep -Fq "$expected" "$server_example" || {
echo "server Compose example lacks required public PostgreSQL/TLS contract: $expected" >&2
return 1
}
done
}
verify_manual_supported_path() {
local profile="$1" manual="$2"
local source_root_export='export THT_SOURCE_ROOT=/absolute/path/to/ThothII'
local bindings_export='export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"'
local generator='"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh" --bindings-env "$THT_WORKSPACE_BINDINGS_ENV_FILE" --operator-env .env --output connector-secrets.local.yaml'
local wrapper='"$THT_SOURCE_ROOT/scripts/compose-with-preflight.sh" --env-file .env'
grep -Fq "$source_root_export" "$manual" || {
echo "$profile manual does not export THT_SOURCE_ROOT for its shell commands" >&2
return 1
}
grep -Fq "$bindings_export" "$manual" || {
echo "$profile manual does not export THT_WORKSPACE_BINDINGS_ENV_FILE for its shell commands" >&2
return 1
}
grep -Fq "$generator" "$manual" || {
echo "$profile manual does not document the connector override generator" >&2
return 1
}
grep -Fq "$wrapper" "$manual" || {
echo "$profile manual does not document the Compose preflight wrapper" >&2
return 1
}
if grep -Eq 'connector-secrets\.workspace-registry|docker compose' "$manual"; then
echo "$profile manual documents a bypassed Compose or copied connector override path" >&2
return 1
fi
echo "$profile manual requires generated connector override and Compose preflight passed"
}
compose_fixture() {
local name="$1" directory="$2"; shift 2
(
cd "$directory"
"$root/scripts/compose-with-preflight.sh" --env-file .env "$@" config --quiet
)
echo "$name passed"
}
prepare_binding_fixture() {
local directory="$1"
printf '%s\n' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT=postgres_direct' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_TRANSPORT=rest_api' \
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE=/run/secrets/north-star-research-vector-api-key' \
>"$directory/workspace-bindings.env"
printf 'THT_WORKSPACE_BINDINGS_ENV_FILE=%s\n' "$directory/workspace-bindings.env" >>"$directory/.env"
}
verify_connector_fixture() {
local directory="$1" rendered project connector_override
project="thoth-install-connector-fixture-$$"
connector_override="$directory/connector-secrets.local.yaml"
"$root/scripts/generate-connector-secrets-override.sh" \
--bindings-env "$directory/workspace-bindings.env" --operator-env "$directory/.env" \
--output "$connector_override" >/dev/null
rendered="$(
cd "$directory"
"$root/scripts/compose-with-preflight.sh" --env-file .env \
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml config
)"
for expected in \
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT: postgres_direct' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE: /run/secrets/north-star-research-dwh-password' \
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE: /run/secrets/north-star-research-vector-api-key' \
'target: north-star-research-dwh-password' \
'target: north-star-research-vector-api-key'; do
grep -Fq "$expected" <<<"$rendered" || {
echo "connector fixture does not give core required binding or secret target: $expected" >&2
return 1
}
done
echo "copied connector binding/secret fixture passed"
if ! (
cd "$directory"
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml run --rm --no-deps --build --entrypoint sh core -c '
test "$THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT" = postgres_direct
test "$THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE" = /run/secrets/north-star-research-dwh-password
test "$THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE" = /run/secrets/north-star-research-vector-api-key
test -f "$THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE"
test -f "$THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE"
'
); then
(
cd "$directory"
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml down --volumes --remove-orphans
) || true
return 1
fi
(
cd "$directory"
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml down --volumes --remove-orphans
)
echo "core process sees connector bindings and secret files passed"
}
verify_documented_operator_path() {
local profile="$1" directory="$2" documented_source_root="$3" connector_override
connector_override="$directory/connector-secrets.local.yaml"
(
cd "$directory"
unset THT_SOURCE_ROOT THT_WORKSPACE_BINDINGS_ENV_FILE
export THT_SOURCE_ROOT="$documented_source_root"
export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"
"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh" \
--bindings-env "$THT_WORKSPACE_BINDINGS_ENV_FILE" --operator-env .env \
--output connector-secrets.local.yaml >/dev/null
"$THT_SOURCE_ROOT/scripts/compose-with-preflight.sh" --env-file .env \
-f compose.workspace-registry.yaml -f git-ssh.workspace-registry.yaml \
-f connector-secrets.local.yaml config --quiet
)
echo "$profile documented shell environment fixture passed"
}
verify_copied_operator_fixtures() {
local fixture_root local_dir server_dir https_dir ssh_dir connector_dir
fixture_root="$(mktemp -d "${TMPDIR%/}/thoth-install-fixtures.XXXXXX")"
trap 'rm -rf "$fixture_root"' RETURN
local_dir="$fixture_root/local"; server_dir="$fixture_root/server"
https_dir="$fixture_root/https"; ssh_dir="$fixture_root/ssh"; connector_dir="$fixture_root/connector"
mkdir -p "$local_dir" "$server_dir" "$https_dir" "$ssh_dir" "$connector_dir"
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$local_dir/compose.workspace-registry.yaml"
printf 'THT_SOURCE_ROOT=%s\n' "$root" >"$local_dir/.env"
prepare_binding_fixture "$local_dir"
compose_fixture "copied local base fixture" "$local_dir" -f compose.workspace-registry.yaml
cp "$root/docs/install/examples/server-compose.workspace-registry.yaml" "$server_dir/compose.workspace-registry.yaml"
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$server_dir/server-sessions.yaml"
: >"$server_dir/session-runtime-password"; : >"$server_dir/session-ca.pem"
printf '%s\n' \
"THT_SOURCE_ROOT=$root" \
"THT_SERVER_WORKSPACE_CONFIG=$server_dir/server-sessions.yaml" \
'THT_SESSION_DB_HOST=sessions.example.invalid' \
'THT_SESSION_DB_NAME=thoth_sessions' \
'THT_SESSION_RUNTIME_USER=thoth_sessions_app' \
"THT_SESSION_RUNTIME_PASSWORD_SOURCE=$server_dir/session-runtime-password" \
"THT_SESSION_CA_SOURCE=$server_dir/session-ca.pem" >"$server_dir/.env"
prepare_binding_fixture "$server_dir"
compose_fixture "copied server PostgreSQL/TLS fixture" "$server_dir" -f compose.workspace-registry.yaml
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$https_dir/compose.workspace-registry.yaml"
cp "$root/docs/install/examples/git-https.workspace-registry.yaml" "$https_dir/git-https.yaml"
: >"$https_dir/git-credentials"; : >"$https_dir/git-ca.pem"
printf '%s\n' \
"THT_SOURCE_ROOT=$root" \
"THT_WORKSPACE_GIT_CREDENTIALS_FILE=$https_dir/git-credentials" \
"THT_WORKSPACE_GIT_CA_FILE=$https_dir/git-ca.pem" >"$https_dir/.env"
prepare_binding_fixture "$https_dir"
compose_fixture "copied HTTPS Git override fixture" "$https_dir" -f compose.workspace-registry.yaml -f git-https.yaml
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$ssh_dir/compose.workspace-registry.yaml"
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$ssh_dir/git-ssh.yaml"
: >"$ssh_dir/git-ssh-key"; : >"$ssh_dir/git-known-hosts"
printf '%s\n' \
"THT_SOURCE_ROOT=$root" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$ssh_dir/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$ssh_dir/git-known-hosts" >"$ssh_dir/.env"
prepare_binding_fixture "$ssh_dir"
compose_fixture "copied SSH Git override fixture" "$ssh_dir" -f compose.workspace-registry.yaml -f git-ssh.yaml
: >"$server_dir/git-ssh-key"; : >"$server_dir/git-known-hosts"
: >"$server_dir/dwh-password"; : >"$server_dir/vector-api-key"
printf '%s\n' \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$server_dir/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$server_dir/git-known-hosts" \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$server_dir/dwh-password" \
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$server_dir/vector-api-key" >>"$server_dir/.env"
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$server_dir/git-ssh.workspace-registry.yaml"
: >"$ssh_dir/dwh-password"; : >"$ssh_dir/vector-api-key"
printf '%s\n' \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$ssh_dir/dwh-password" \
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$ssh_dir/vector-api-key" >>"$ssh_dir/.env"
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$ssh_dir/git-ssh.workspace-registry.yaml"
verify_documented_operator_path local "$ssh_dir" "$root"
verify_documented_operator_path server "$server_dir" "$root"
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$connector_dir/compose.workspace-registry.yaml"
: >"$connector_dir/dwh-password"; : >"$connector_dir/vector-password"
printf '%s\n' \
"THT_SOURCE_ROOT=$root" \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$connector_dir/dwh-password" \
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$connector_dir/vector-password" >"$connector_dir/.env"
prepare_binding_fixture "$connector_dir"
verify_connector_fixture "$connector_dir"
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_FILE=not-a-path\n' >"$fixture_root/non-path-secret.env"
if verify_path_variable_values "$fixture_root/non-path-secret.env" >/dev/null 2>&1; then
echo "non-path secret-file fixture was accepted" >&2
return 1
fi
echo "non-path secret-file fixture rejected passed"
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=literal-value\n' >"$fixture_root/literal-source.env"
if verify_path_variable_values "$fixture_root/literal-source.env" >/dev/null 2>&1; then
echo "literal secret-source fixture was accepted" >&2
return 1
fi
echo "literal secret-source fixture rejected passed"
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=installation-secrets/password\n' >"$fixture_root/relative-source.env"
if verify_path_variable_values "$fixture_root/relative-source.env" >/dev/null 2>&1; then
echo "relative secret-source fixture was accepted" >&2
return 1
fi
echo "relative secret-source fixture rejected passed"
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=/srv/thothii/secrets/../password\n' >"$fixture_root/non-normalized-source.env"
if verify_path_variable_values "$fixture_root/non-normalized-source.env" >/dev/null 2>&1; then
echo "non-normalized secret-source fixture was accepted" >&2
return 1
fi
echo "non-normalized secret-source fixture rejected passed"
}
case "$profile" in
--fixtures-only)
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
verify_manual_supported_path local "$root/docs/install/local-workspace-registry.md"
verify_manual_supported_path server "$root/docs/install/server-workspace-registry.md"
verify_copied_operator_fixtures
exit 0
;;
--profile)
profile="${2:-}"
[[ $# -eq 2 ]] || { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
;;
*)
echo "usage: $0 --profile {local|server}" >&2
exit 2
;;
esac
case "$profile" in
local)
manual="$root/docs/install/local-workspace-registry.md"
example="$root/docs/install/examples/local-compose.workspace-registry.yaml"
verify_manual() {
local profile="$1" manual
manual="$root/docs/install/$profile-workspace-registry.md"
local -a headings
if [[ "$profile" == local ]]; then
headings=(
"Prerequisites"
"Git remote: SSH and HTTPS"
@@ -310,10 +57,7 @@ case "$profile" in
"Publish, update, backup, outage recovery, and rollback"
"Troubleshooting"
)
;;
server)
manual="$root/docs/install/server-workspace-registry.md"
example="$root/docs/install/examples/server-compose.workspace-registry.yaml"
else
headings=(
"Service account, storage, and firewall"
"Gitea and remote Git setup"
@@ -324,50 +68,186 @@ case "$profile" in
"Pull, publish, upgrade, backup, and recovery"
"Troubleshooting and snapshot rollback"
)
fi
for heading in "${headings[@]}"; do
grep -Fqx "## $heading" "$manual" || {
echo "missing required heading in $profile manual: $heading" >&2
return 1
}
done
for expected in \
'export THT_SOURCE_ROOT=/absolute/path/to/ThothII' \
'--env-file "$THT_OPERATOR_ENV"' \
"-f \"\$THT_SOURCE_ROOT/compose.yaml\" -f \"\$THT_SOURCE_ROOT/deploy/compose.$profile.yaml\"" \
'"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh"'; do
grep -Fq -- "$expected" "$manual" || {
echo "$profile manual lacks canonical operator step: $expected" >&2
return 1
}
done
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' "$manual"; then
echo "$profile manual documents a superseded or bypassed Compose path" >&2
return 1
fi
verify_path_variable_values "$manual"
echo "$profile manual canonical base+override references passed"
}
write_private() {
local path="$1" value="$2"
printf '%s\n' "$value" >"$path"
chmod 0600 "$path"
}
verify_compose_fixtures() {
local fixture connector_override profile rendered
fixture="$(mktemp -d "${TMPDIR%/}/thoth-install-fixtures.XXXXXX")"
trap 'rm -rf "$fixture"' RETURN
mkdir -p "$fixture/data" "$fixture/pi-state" "$fixture/workspace-registry"
write_private "$fixture/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}'
write_private "$fixture/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
write_private "$fixture/git-ssh-key" 'fixture-git-ssh-key'
write_private "$fixture/git-known-hosts" 'fixture-git-known-hosts'
write_private "$fixture/dwh-password" 'fixture-dwh-password'
write_private "$fixture/vector-api-key" 'fixture-vector-api-key'
write_private "$fixture/session-runtime-password" 'fixture-session-runtime-password'
write_private "$fixture/session-migrator-password" 'fixture-session-migrator-password'
write_private "$fixture/session-ca.pem" 'fixture-session-ca'
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$fixture/server-sessions.yaml"
printf '%s\n' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT=postgres_direct' \
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_TRANSPORT=rest_api' \
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE=/run/secrets/north-star-research-vector-api-key' \
>"$fixture/workspace-bindings.env"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$fixture/pi-auth.json" \
"THT_SECRETS_FILE=$fixture/thothii.secrets" \
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture/workspace-bindings.env" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture/git-known-hosts" \
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture/dwh-password" \
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$fixture/vector-api-key" \
"THT_DATA_ROOT=$fixture/data" \
"THT_PI_STATE_ROOT=$fixture/pi-state" \
"THT_WORKSPACE_REGISTRY_ROOT=$fixture/workspace-registry" \
"THT_SERVER_WORKSPACE_CONFIG=$fixture/server-sessions.yaml" \
'THT_SESSION_DB_HOST=sessions.example.invalid' \
'THT_SESSION_DB_NAME=thoth_sessions' \
'THT_SESSION_RUNTIME_USER=thoth_sessions_app' \
'THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate' \
"THT_SESSION_RUNTIME_PASSWORD_SOURCE=$fixture/session-runtime-password" \
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$fixture/session-migrator-password" \
"THT_SESSION_CA_SOURCE=$fixture/session-ca.pem" \
>"$fixture/operator.env"
connector_override="$fixture/connector-secrets.local.yaml"
"$root/scripts/generate-connector-secrets-override.sh" \
--bindings-env "$fixture/workspace-bindings.env" \
--operator-env "$fixture/operator.env" \
--output "$connector_override" >/dev/null
for profile in local server; do
rendered="$fixture/$profile.json"
files=(
-f "$root/compose.yaml"
-f "$root/deploy/compose.$profile.yaml"
)
if [[ "$profile" == server ]]; then
files+=(-f "$root/deploy/compose.session-server.yaml.example")
fi
files+=(
-f "$root/deploy/compose.git-ssh.yaml"
-f "$connector_override"
)
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture/operator.env" \
"${files[@]}" config --format json >"$rendered"
node - "$rendered" "$profile" <<'NODE'
const fs = require("fs");
const [path, profile] = process.argv.slice(2);
const config = JSON.parse(fs.readFileSync(path, "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
throw new Error(profile + ": mandatory stack must be exactly core,frontend");
}
const core = config.services.core;
for (const target of [
"/home/thoth/.pi/agent/auth.json",
"/home/thoth/.pi/agent/models.json",
"/home/thoth/.pi/agent/settings.json",
]) {
if (!(core.volumes || []).some((mount) => mount.target === target && mount.read_only)) {
throw new Error(profile + ": missing read-only Pi mount " + target);
}
}
for (const [name, value] of Object.entries({
THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE: "/run/secrets/north-star-research-dwh-password",
THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE: "/run/secrets/north-star-research-vector-api-key",
})) {
if (core.environment?.[name] !== value) throw new Error(profile + ": missing binding " + name);
}
const secretTargets = new Set((core.secrets || []).map((secret) => secret.target));
for (const target of [
"thothii.secrets",
"north-star-research-dwh-password",
"north-star-research-vector-api-key",
]) {
if (!secretTargets.has(target)) throw new Error(profile + ": missing secret target " + target);
}
if (profile === "server") {
for (const target of ["session_runtime_password", "session_ca.pem"]) {
if (!secretTargets.has(target)) throw new Error("server: missing session secret " + target);
}
}
if ((config.services.frontend.secrets || []).length !== 0) {
throw new Error(profile + ": frontend received a runtime secret");
}
const rendered = JSON.stringify(config);
for (const value of [
"fixture-native-auth-key", "fixture-model-api-key", "fixture-git-ssh-key",
"fixture-git-known-hosts", "fixture-dwh-password", "fixture-vector-api-key",
"fixture-session-runtime-password", "fixture-session-migrator-password", "fixture-session-ca",
]) {
if (rendered.includes(value)) throw new Error(profile + ": rendered Compose leaked " + value);
}
NODE
echo "canonical $profile base+override fixture passed"
done
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=relative/secret\n' >"$fixture/unsafe.env"
if verify_path_variable_values "$fixture/unsafe.env" >/dev/null 2>&1; then
echo "relative secret-source fixture was accepted" >&2
return 1
fi
echo "relative secret-source fixture rejected passed"
}
case "$mode" in
--fixtures-only)
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
verify_manual local
verify_manual server
verify_compose_fixtures
;;
--profile)
profile="${2:-}"
[[ $# -eq 2 && "$profile" =~ ^(local|server)$ ]] \
|| { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
verify_manual "$profile"
verify_compose_fixtures
echo "== Run isolated workspace-registry bootstrap and recovery smoke =="
(
cd "$root"
env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh
)
echo "$profile installation documentation verification passed"
;;
*)
echo "unknown documentation profile: $profile" >&2
echo "usage: $0 --fixtures-only | --profile {local|server}" >&2
exit 2
;;
esac
[[ -f "$manual" ]] || { echo "missing $profile installation manual: $manual" >&2; exit 1; }
[[ -f "$example" ]] || { echo "missing $profile Compose example: $example" >&2; exit 1; }
for heading in "${headings[@]}"; do
grep -Fqx "## $heading" "$manual" >/dev/null || {
echo "missing required heading in $profile manual: $heading" >&2
exit 1
}
done
grep -Fq "$(basename "$example")" "$manual" || {
echo "the $profile manual does not reference its Compose example" >&2
exit 1
}
# Values for secret-bearing variables must be paths. These patterns catch common accidental
# credentials while allowing declarative *_FILE bindings and explicitly empty assignments.
if grep -Ein '(^|[[:space:]])(password|api[_-]?key|token|secret)[[:space:]]*[:=][[:space:]]*[^[:space:]#]' \
"$manual" "$example" >/dev/null; then
echo "installation documentation contains a secret literal" >&2
exit 1
fi
verify_path_variable_values "$manual"
verify_path_variable_values "$example"
verify_path_variable_values "$root/docs/install/examples/git-https.workspace-registry.yaml"
verify_path_variable_values "$root/docs/install/examples/git-ssh.workspace-registry.yaml"
verify_path_variable_values "$root/docs/install/examples/workspace-bindings.env.example"
verify_server_public_contract
verify_manual_supported_path "$profile" "$manual"
echo "== Validate copied operator fixtures and documented optional Git transports =="
verify_copied_operator_fixtures
echo "== Run isolated workspace-registry bootstrap and recovery smoke =="
(
cd "$root"
env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh
)
echo "$profile installation documentation verification passed"