Files
ThothII/scripts/verify-workspace-install-docs.sh
T

2326 lines
102 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verify canonical local/server installation manuals and their base+override Compose paths.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd -P)"
mode="${1:-}"
tmp_prefix="${TMPDIR:-/tmp}"
while [[ "$tmp_prefix" != "/" && "$tmp_prefix" == */ ]]; do
tmp_prefix="${tmp_prefix%/}"
done
tmp_prefix="${tmp_prefix%/}/"
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
is_safe_absolute_path() {
local value="$1" segment
local -a segments
[[ "$value" == /* && "$value" != *//* ]] || return 1
IFS=/ read -r -a segments <<<"$value"
for segment in "${segments[@]}"; do
[[ "$segment" != . && "$segment" != .. ]] || return 1
done
}
verify_path_variable_values() {
local source="$1" line trimmed name value
while IFS= read -r line || [[ -n "$line" ]]; do
trimmed="$(trim "$line")"
if [[ "$trimmed" == *=* || "$trimmed" == *:* ]]; then
name="$(trim "${trimmed%%[=:]*}")"
value="$(trim "${trimmed#"$name"}")"
value="$(trim "${value#[:=]}")"
if [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*_(FILE|SOURCE)$ ]]; then
value="$(trim "${value%%#*}")"
value="${value#\"}"; value="${value%\"}"
value="${value#\'}"; value="${value%\'}"
if [[ -n "$value" ]] && ! is_safe_absolute_path "$value"; then
echo "unsafe path value for $name in $source" >&2
return 1
fi
fi
fi
done <"$source"
}
require_absent() {
local source="$1" label="$2"
shift 2
local forbidden
for forbidden in "$@"; do
if grep -Fq -- "$forbidden" "$source"; then
echo "$label contains forbidden text: $forbidden" >&2
return 1
fi
done
}
require_pattern() {
local source="$1" label="$2" pattern="$3"
python3 - "$source" "$label" "$pattern" <<'PY'
import pathlib, re, sys
source = pathlib.Path(sys.argv[1]).read_text()
label = sys.argv[2]
pattern = sys.argv[3]
if not re.search(pattern, source, re.MULTILINE | re.DOTALL):
raise SystemExit(f"{label} lacks required pattern: {pattern}")
PY
}
require_headings() {
local source="$1" label="$2"
shift 2
local heading
for heading in "$@"; do
grep -Fqx "## $heading" "$source" || {
echo "missing required heading in $label: $heading" >&2
return 1
}
done
}
require_text() {
local source="$1" label="$2"
shift 2
local expected
for expected in "$@"; do
grep -Fq -- "$expected" "$source" || {
echo "$label lacks required instruction: $expected" >&2
return 1
}
done
}
require_concept_tokens() {
local source="$1" label="$2"
shift 2
python3 - "$source" "$label" "$@" <<'PY'
import pathlib, re, sys
text = pathlib.Path(sys.argv[1]).read_text().lower()
label = sys.argv[2]
tokens = [t.lower() for t in sys.argv[3:]]
for token in tokens:
if token not in text:
raise SystemExit(f"{label} lacks required concept token: {token}")
PY
}
verify_workspace_descriptor_doc_contract() {
local source="$1" label="$2"
if ! PYTHONDONTWRITEBYTECODE=1 python3 "$root/scripts/workspace_descriptor_doc_contract.py" --document "$source"; then
echo "$label violates the workspace descriptor documentation contract" >&2
return 1
fi
}
verify_markdown_table_relationships() {
local source="$1" label="$2" heading="$3" spec_json="$4"
python3 - "$source" "$label" "$heading" "$spec_json" <<'PY'
import json, pathlib, re, sys
path = pathlib.Path(sys.argv[1])
label = sys.argv[2]
heading = sys.argv[3]
spec = json.loads(sys.argv[4])
text = path.read_text()
match = re.search(rf"^##+\s+{re.escape(heading)}\s*$", text, re.MULTILINE)
if not match:
raise SystemExit(f"{label}: missing structured section '{heading}'")
lines = text[match.end():].splitlines()
table = []
for line in lines:
if not line.strip():
if table:
break
continue
if not line.lstrip().startswith("|"):
if table:
break
continue
table.append(line.rstrip())
if len(table) < 3:
raise SystemExit(f"{label}: structured table '{heading}' is incomplete")
headers = [cell.strip().lower() for cell in table[0].strip().strip("|").split("|")]
rows = []
for raw in table[2:]:
cells = [cell.strip() for cell in raw.strip().strip("|").split("|")]
if len(cells) != len(headers):
raise SystemExit(f"{label}: malformed row in '{heading}'")
rows.append(dict(zip(headers, cells)))
for row_spec in spec["rows"]:
found = False
for row in rows:
ok = True
for column, pattern in row_spec.items():
value = row.get(column.lower(), "")
if not re.search(pattern, value, re.IGNORECASE | re.DOTALL):
ok = False
break
if ok:
found = True
break
if not found:
raise SystemExit(f"{label}: missing relationship in '{heading}': {row_spec}")
PY
}
semantic_index_relationship_spec() {
cat <<'JSON'
{"rows":[
{"scope":"workspace semantic index","ownership rule":"(each|one|single|exactly one).*(workspace).*(single|one|exactly one).*(Qdrant).*(collection)|(workspace keeps exactly one qdrant collection reserved for itself)","isolation rule":"schema.*evidence.*memory.*(one|that).*(collection).*(kind|payload)|schema.*evidence.*memory.*together.*collection.*(kind|payload)"}
]}
JSON
}
verify_compose_internal_semantic_contract() {
python3 - "$root/compose.yaml" <<'PY'
import sys, yaml, pathlib
doc = yaml.safe_load(pathlib.Path(sys.argv[1]).read_text())
services = doc["services"]
expected = {"core", "frontend", "qdrant", "embedding", "embedding-model-init", "workspace-maintenance"}
if set(services) != expected:
raise SystemExit(f"compose.yaml services mismatch: {sorted(services)}")
if doc["services"]["workspace-maintenance"].get("profiles") != ["workspace-maintenance"]:
raise SystemExit("workspace-maintenance must be profile-gated and absent from default startup")
core = services["core"]
env = core["environment"]
for key, value in {
"THT_INTERNAL_QDRANT_URL": "http://qdrant:6333",
"THT_INTERNAL_EMBEDDING_URL": "http://embedding:11434",
"THT_INTERNAL_EMBEDDING_MODEL": "qwen3-embedding:0.6b",
"THT_INTERNAL_EMBEDDING_DIMENSIONS": "1024",
}.items():
if env.get(key) != value:
raise SystemExit(f"core missing semantic env {key}={value}")
for forbidden in ("THT_VEC_REST_URL", "THT_VEC_WRITE_REST_URL", "THT_OLLAMA_URL"):
if forbidden in env:
raise SystemExit(f"core still exposes deprecated env {forbidden}")
if core["depends_on"]["qdrant"]["condition"] != "service_healthy":
raise SystemExit("core must wait for qdrant health")
if core["depends_on"]["embedding-model-init"]["condition"] != "service_completed_successfully":
raise SystemExit("core must wait for model init success")
for name, port in (("qdrant", "6333"), ("embedding", "11434")):
service = services[name]
if "ports" in service:
raise SystemExit(f"{name} must stay private")
if service.get("expose") != [port]:
raise SystemExit(f"{name} expose mismatch")
if "devices" in str(services["embedding"]):
raise SystemExit("base embedding service must stay CPU-first")
volumes = set(doc["volumes"])
for required in ("qdrant-data", "embedding-models", "workspace-secrets"):
if required not in volumes:
raise SystemExit(f"missing volume {required}")
model_init = services["embedding-model-init"]
if model_init["environment"].get("OLLAMA_MODEL") != "qwen3-embedding:0.6b":
raise SystemExit("model init must pin qwen3-embedding:0.6b")
PY
}
verify_workspace_descriptor_semantic_contract() {
local source="${1:?source required}"
local label="${2:-$source}"
python3 - "$source" "$label" <<'PY'
import pathlib, sys, yaml
path = pathlib.Path(sys.argv[1])
label = sys.argv[2]
doc = yaml.safe_load(path.read_text())
ws = doc["workspace"]
semantic = doc["semantic_index"]
vector = semantic["vector_store"]
embedding = semantic["embedding"]
if ws["schema_version"] != 3:
raise SystemExit(f"{label}: schema_version must be 3")
if vector["engine"] != "qdrant":
raise SystemExit(f"{label}: vector store must be qdrant")
if vector["collection"] != ws["id"]:
raise SystemExit(f"{label}: collection must equal workspace id")
if vector["dimensions"] != 1024 or vector["distance"] != "cosine":
raise SystemExit(f"{label}: vector contract must be 1024/cosine")
if embedding["provider"] != "ollama_internal":
raise SystemExit(f"{label}: embedding provider must be ollama_internal")
if embedding["model"] != "qwen3-embedding:0.6b":
raise SystemExit(f"{label}: embedding model must be qwen3-embedding:0.6b")
if embedding["dimensions"] != 1024:
raise SystemExit(f"{label}: embedding dimensions must be 1024")
PY
}
verify_workspace_evidence_contract() {
local base_root="${1:-$root}"
python3 - "$base_root" <<'PY'
import pathlib, re, sys, yaml
from pathlib import PurePosixPath
base = pathlib.Path(sys.argv[1])
contract_path = base / "docs/contracts/workspace-evidence-v3.md"
local_path = base / "docs/install/local-workspace-registry.md"
server_path = base / "docs/install/server-workspace-registry.md"
readme_path = base / "README.md"
migration_path = base / "docs/migrations/p1-to-p1-1-registry-layout.md"
bindings_path = base / "docs/install/examples/workspace-bindings.env.example"
descriptor_paths = [
base / "deploy/workspaces/example.yaml",
base / "deploy/workspaces/psd.yaml.example",
]
paths = [contract_path, local_path, server_path, readme_path, migration_path, bindings_path, *descriptor_paths]
for path in paths:
if not path.is_file():
raise SystemExit(f"missing workspace Evidence contract input: {path.relative_to(base)}")
for path in descriptor_paths:
relative = path.relative_to(base).as_posix()
document = yaml.safe_load(path.read_text())
workspace_id = document["workspace"]["id"]
evidence = document.get("evidence")
if not isinstance(evidence, dict) or not isinstance(evidence.get("source"), dict):
raise SystemExit(f"{relative}: missing explicit filesystem Evidence contract")
uri = evidence["source"].get("uri")
expected_uri = f"{workspace_id}/evidence"
if not isinstance(uri, str) or uri.startswith("/") or "\\" in uri or ".." in uri.split("/"):
raise SystemExit(f"{relative}: noncanonical filesystem Evidence URI")
if uri != expected_uri:
raise SystemExit(f"{relative}: Evidence namespace mismatch")
expected = {
"source": {
"type": "filesystem",
"uri": expected_uri,
"patterns": ["**/*.md"],
"max_bytes": 10485760,
},
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
}
if evidence != expected:
raise SystemExit(f"{relative}: explicit filesystem Evidence object mismatch")
contract = contract_path.read_text()
readme = readme_path.read_text()
migration = migration_path.read_text()
all_public = "\n".join(path.read_text() for path in paths)
active_public = "\n".join(path.read_text() for path in [contract_path, local_path, server_path, readme_path, bindings_path, *descriptor_paths])
def normalize_space(text: str) -> str:
return re.sub(r"\s+", " ", text.strip())
def named_example(name):
match = re.search(
rf"^### Example: {re.escape(name)}\s*$\n\s*```yaml\n(.*?)^```\s*$",
contract,
re.MULTILINE | re.DOTALL,
)
if not match:
raise SystemExit(f"missing named {name} Evidence YAML example")
return yaml.safe_load(match.group(1))
examples = {
"filesystem": {
"evidence": {
"source": {
"type": "filesystem", "uri": "example/evidence",
"patterns": ["**/*.md"], "max_bytes": 10485760,
},
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
},
},
"http": {
"evidence": {
"source": {
"type": "http", "uris": ["https://evidence.example.invalid/report.md"],
"authentication": "signed_urls_file", "connect_timeout_ms": 5000,
"read_timeout_ms": 30000, "max_bytes": 10485760, "max_redirects": 5,
"allow_private_hosts": False, "max_cache_bytes": 67108864,
},
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
},
},
"s3": {
"evidence": {
"source": {
"type": "s3", "uri": "s3://example-evidence/curated/",
"credentials": "static_files", "trusted_endpoint": False,
"allow_private_endpoint": False, "allow_insecure_endpoint": False,
"max_bytes": 10485760, "max_objects": 10000, "max_pages": 100,
"page_size": 1000,
},
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
},
},
}
for name, expected in examples.items():
if named_example(name) != expected:
raise SystemExit(f"{name} Evidence YAML example shape/default mismatch")
required_contract_phrases = [
"Evidence is optional: a valid v3 descriptor without it remains operational.",
"reject unknown keys",
"nonempty list of unique, normalized relative POSIX globs",
"no whitespace, control character, backslash, userinfo, query, or fragment",
"A custom endpoint requires",
"HTTP endpoint additionally requires",
"page size cannot exceed 1000",
"Public docs, APIs, and rendered YAML never expose file contents.",
"THT_WORKSPACE_SECRET_ROOTS",
"readable regular file",
"strictly below",
"Content-only revision",
"`schema_version` value `1`",
"It is authoritative for workspace ID,\nname, description, and display order.",
"The descriptor at `<id>/workspace.yaml` must match the\ncatalog metadata exactly.",
"catalog-only entries are invalid and reject the complete candidate revision.",
"The API never writes `thoth-workspaces.yaml`,\n`<id>/workspace.yaml`, `<id>/schema/**`, or `<id>/evidence/**`.",
]
normalized_contract = normalize_space(contract)
for phrase in required_contract_phrases:
if normalize_space(phrase) not in normalized_contract:
raise SystemExit(f"workspace Evidence contract lacks required rule: {phrase}")
mode_rules = {
"missing public HTTP mode": "Public HTTP (`authentication: none`) uses the declared query-free\nURIs directly and requires no Evidence credential file.",
"missing ambient S3 mode": "Ambient S3\n(`credentials: ambient`) uses the runtime provider chain and requires no Evidence credential file.",
}
for error, phrase in mode_rules.items():
if phrase not in contract:
raise SystemExit(error)
if "positive safe integers" not in contract or "nonnegative safe integer" not in contract or "9007199254740991" not in contract:
raise SystemExit("missing strict Evidence numeric domains")
if "Endpoint-policy flags cannot be enabled without `endpoint_url`." not in contract:
raise SystemExit("missing S3 endpoint policy without endpoint invariant")
for forbidden in (
"workspace-content/<id>/evidence",
"workspaces/<id>.yaml",
"workspace-content/example/evidence",
"Validate and publish the descriptor against that base commit",
):
if forbidden in active_public:
raise SystemExit("old registry layout text found")
required_tree_lines = [
"workspace-repository.git/", "├── thoth-workspaces.yaml", "├── example/",
"│ ├── workspace.yaml", "│ └── evidence/...", "└── another/",
" └── workspace.yaml",
]
if any(line not in contract for line in required_tree_lines):
raise SystemExit("missing canonical Evidence layout")
def table_for(heading):
match = re.search(rf"^## {re.escape(heading)}\s*$", contract, re.MULTILINE)
if not match:
raise SystemExit(f"missing structured Evidence section: {heading}")
rows = []
for line in contract[match.end():].splitlines():
if line.startswith("## "):
break
if line.startswith("|"):
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if len(cells) >= 2 and not all(set(cell) <= {"-", ":"} for cell in cells):
rows.append(cells)
return rows[1:] if rows else []
relationships = {row[0]: row[1] for row in table_for("Registry revision and phase ownership")}
revision_text = relationships.get("Revision identity", "")
if not all(token in revision_text for token in ("same 40-hex Git commit", "catalog blob", "descriptor blob", "root tree")):
raise SystemExit("missing same-revision ownership")
if "Evidence-only commit" not in relationships.get("Content-only revision", "") or "revision.commit" not in relationships.get("Content-only revision", ""):
raise SystemExit("missing content-only revision identity")
repository_consumer = relationships.get("Repository consumer", "")
if not all(token in repository_consumer for token in ("complete candidate", "atomically activates", "never edits, commits, or pushes")):
raise SystemExit("missing read-only repository-consumer rule")
runtime_secrets = relationships.get("Runtime secrets", "")
if not all(token in runtime_secrets for token in ("configured/missing status only", "runtime lease")):
raise SystemExit("missing runtime-secret lifecycle rule")
p11 = relationships.get("P1.1", "")
p6 = relationships.get("P6", "")
if not all(token in p11 for token in ("lexical URI `<id>/evidence`", "Git tree", "same commit", "does not recursively inspect nested symlinks", "out of scope for P1.1")):
raise SystemExit("missing P1.1 lexical/tree ownership")
if not all(token in p6 for token in ("commit-addressed materialization", "realpath", "recursive containment", "nested-symlink", "race")):
raise SystemExit("missing P6 materialization ownership")
no_scope = "P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, active-snapshot retention, or GC."
p1_adverbs = r"(?:\s+(?:also|then|now|directly|itself))*"
p1_base_operation = r"""(?:
acquire|materialize|extract|preprocess|index|retain|
(?:create|generate)\s+embeddings?|
write\s+(?:embeddings?\s+)?to\s+Qdrant|
publish\s+`?ACTIVE\b`?|
garbage[- ]collect|
(?:run|perform)\s+(?:retention|GC|garbage[ -]collection)
)"""
p1_third_person_operation = r"""(?:
acquires|materializes|extracts|preprocesses|indexes|retains|
(?:creates|generates)\s+embeddings?|
writes\s+(?:embeddings?\s+)?to\s+Qdrant|
publishes\s+`?ACTIVE\b`?|
garbage[- ]collects|
(?:runs|performs)\s+(?:retention|GC|garbage[ -]collection)
)"""
p1_ownership = r"""(?:
(?:owns|handles|performs)|is\s+responsible\s+for
)\s+(?:Evidence\s+)?(?:
acquisition|materialization|extraction|preprocessing|embeddings?|
Qdrant\s+writes?|indexing|`?ACTIVE`?\s+publication|retention|GC|
garbage[ -]collection
)"""
positive_p1_operation = re.compile(
rf"""\bP1(?:\.1)?\b{p1_adverbs}\s+(?:
(?:(?:can|may|must|will|should|does){p1_adverbs}\s+){p1_base_operation}|
{p1_third_person_operation}|
{p1_ownership}
)\b""",
re.IGNORECASE | re.VERBOSE,
)
if normalize_space(no_scope) not in normalized_contract or positive_p1_operation.search(contract):
raise SystemExit("P1.1 scope violation")
installation_rows = {row[0]: row[1:] for row in table_for("Installation files")}
http_row = " ".join(installation_rows.get("Signed HTTP", []))
if "THT_WS_<NAMESPACE>_EVIDENCE_SIGNED_URLS_FILE" not in http_row or not all(
token in http_row for token in (
"1048576 bytes", "nonempty UTF-8 JSON string array", "declared-URI order",
"query-stripped identities", "one-to-one",
)
):
raise SystemExit("missing signed HTTP file boundary")
s3_pair = " ".join(installation_rows.get("Static S3 pair", []))
if not all(token in s3_pair for token in (
"THT_WS_<NAMESPACE>_EVIDENCE_ACCESS_KEY_FILE",
"THT_WS_<NAMESPACE>_EVIDENCE_SECRET_KEY_FILE", "Required together", "65536 bytes",
)):
raise SystemExit("missing static S3 file boundary")
s3_token = " ".join(installation_rows.get("Static S3 session", []))
if not all(token in s3_token for token in (
"THT_WS_<NAMESPACE>_EVIDENCE_SESSION_TOKEN_FILE", "Optional", "65536 bytes",
)):
raise SystemExit("missing static S3 session-token boundary")
if "tht config check -c <path>" not in contract:
raise SystemExit("exact config-check ordering missing")
automated = re.findall(r"^automated integration: (?:PENDING|PASS|FAIL)$", contract, re.MULTILINE)
manual = re.findall(r"^manual acceptance: (?:PENDING|PASS|FAIL)$", contract, re.MULTILINE)
if len(automated) != 1 or len(manual) != 1:
raise SystemExit("separate automated/manual states missing")
flow_tokens = [
"Create a local workspace",
"thoth-workspaces.yaml",
"<workspace-id>/workspace.yaml",
"commit",
"push",
"ThothII",
"Update workspace repository",
"workspace-secrets",
"Validate workspace source",
"Test workspace connections",
"Save entered secrets",
"Forget stored value",
]
for guide in (local_path, server_path):
text = guide.read_text()
match = re.search(
r"^## Prepare and publish a workspace source\s*$\n(.*?)(?=^## |\Z)",
text,
re.MULTILINE | re.DOTALL,
)
if not match:
raise SystemExit(f"{guide.name}: missing workspace source flow")
section = text
positions = [section.find(token) for token in flow_tokens]
if any(position < 0 for position in positions):
raise SystemExit(f"{guide.name}: curator flow missing registry rule")
readme_required = [
"thoth-workspaces.yaml",
"<id>/workspace.yaml",
"<id>/evidence/**",
"authoritative for workspace ID, name, description, and\ndisplay order",
"the complete candidate is rejected",
"ThothII\nnever writes any workspace repository content.",
"docs/migrations/p1-to-p1-1-registry-layout.md",
]
normalized_readme = normalize_space(readme)
for phrase in readme_required:
if normalize_space(phrase) not in normalized_readme:
raise SystemExit("README registry overview incomplete")
migration_commit_phrases = [
"git mv workspaces/<id>.yaml <id>/workspace.yaml",
"git mv workspace-content/<id>/evidence <id>/evidence",
"create and review thoth-workspaces.yaml from descriptor metadata",
]
for phrase in migration_commit_phrases:
if phrase not in migration:
raise SystemExit("migration guide missing commit step")
if "Upgrade ThothII only after that migration commit is pushed." not in migration:
raise SystemExit("migration guide missing upgrade ordering")
if "Roll back the application revision and registry commit together." not in migration:
raise SystemExit("migration guide missing rollback rule")
if "reject the old flat layout and a\nrepository without `thoth-workspaces.yaml`" not in migration:
raise SystemExit("migration guide missing rejection rule")
aws_access_key = re.compile(
r"(?<![A-Za-z0-9_])(?:AKIA|ASIA)[A-Z0-9]{16}(?![A-Za-z0-9_])"
)
if aws_access_key.search(all_public):
raise SystemExit("credential literal forbidden")
def dotenv_lines(path):
text = path.read_text()
if path == bindings_path:
sources = [text]
else:
sources = re.findall(r"```(?:dotenv|sh)\n(.*?)```", text, re.DOTALL)
assignments = []
for source in sources:
for line in source.splitlines():
match = re.match(r"\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$", line)
if match:
assignments.append((match.group(1), match.group(2).strip().strip("\"'")))
return assignments
def safe_absolute(value):
if not value.startswith("/") or "//" in value:
return False
return all(part not in (".", "..") for part in PurePosixPath(value).parts)
assignments = []
for path in (bindings_path, local_path, server_path):
assignments.extend(dotenv_lines(path))
unsafe_placeholders = ("changeme", "replace-me", "your_secret", "<secret>")
for name, value in assignments:
lowered = value.lower()
if any(token in lowered for token in unsafe_placeholders):
raise SystemExit("unsafe file placeholder/path")
if name.endswith(("_FILE", "_SOURCE")) and value and not safe_absolute(value):
raise SystemExit("unsafe file placeholder/path")
if "_EVIDENCE_" in name and name.endswith("_FILE") and not value.startswith("/run/secrets/"):
raise SystemExit("unsafe file placeholder/path")
if "_EVIDENCE_" in name and name.endswith("_SOURCE") and not (
value.startswith("/srv/thothii/secrets/")
or value.startswith("/absolute/path/installation-secrets/")
):
raise SystemExit("unsafe file placeholder/path")
credential_name = re.search(r"(?:SECRET_KEY|ACCESS_KEY|PASSWORD|SESSION_TOKEN|SIGNED_URLS|CREDENTIAL)$", name)
if credential_name and value:
raise SystemExit("credential literal forbidden")
if re.match(r"(?i)(?:AKIA|ASIA)[A-Z0-9]{12,}", value):
raise SystemExit("credential literal forbidden")
if re.match(r"https?://", value) and "?" in value:
raise SystemExit("query-bearing public URI forbidden")
for uri in re.findall(r"https?://[^\s`\"'<>]+", all_public):
if "?" in uri:
raise SystemExit("query-bearing public URI forbidden")
authority = uri.split("//", 1)[1].split("/", 1)[0]
if "@" in authority:
raise SystemExit("credential literal forbidden")
expected_evidence_bindings = {
"THT_WS_SIGNED_HTTP_EVIDENCE_SIGNED_URLS_FILE": "/run/secrets/signed-http-evidence-urls.json",
"THT_WS_STATIC_S3_EVIDENCE_ACCESS_KEY_FILE": "/run/secrets/static-s3-evidence-access-key",
"THT_WS_STATIC_S3_EVIDENCE_SECRET_KEY_FILE": "/run/secrets/static-s3-evidence-secret-key",
"THT_WS_STATIC_S3_EVIDENCE_SESSION_TOKEN_FILE": "/run/secrets/static-s3-evidence-session-token",
}
binding_values = dict(dotenv_lines(bindings_path))
for name, value in expected_evidence_bindings.items():
if binding_values.get(name) != value:
raise SystemExit(f"workspace bindings example mismatch: {name}")
print("workspace Evidence documentation contract passed")
PY
}
verify_vector_helper_interfaces() {
local output status
output="$(mktemp "${tmp_prefix}thoth-vector-backup-help.XXXXXX")"
set +e
"$root/scripts/vector-backup.sh" >"$output" 2>&1
status=$?
set -e
[[ $status -eq 2 ]] || { cat "$output" >&2; rm -f "$output"; echo "vector-backup usage exit mismatch" >&2; return 1; }
grep -Eq 'usage: .*--project-name NAME --output FILE' "$output" || { cat "$output" >&2; rm -f "$output"; echo "vector-backup usage contract changed" >&2; return 1; }
set +e
"$root/scripts/vector-restore.sh" >"$output" 2>&1
status=$?
set -e
[[ $status -eq 2 ]] || { cat "$output" >&2; rm -f "$output"; echo "vector-restore usage exit mismatch" >&2; return 1; }
grep -Eq 'usage: .*--project-name NAME --input FILE --confirm-project NAME' "$output" || { cat "$output" >&2; rm -f "$output"; echo "vector-restore usage contract changed" >&2; return 1; }
rm -f "$output"
}
verify_project_state_current_contract() {
local source="${1:-$root/PROJECT_STATE.md}"
local label="${2:-PROJECT_STATE.md}"
if ! PYTHONDONTWRITEBYTECODE=1 python3 "$root/scripts/workspace_descriptor_doc_contract.py" --project-state "$source"; then
echo "$label violates the workspace descriptor documentation contract" >&2
return 1
fi
python3 - "$source" "$label" <<'PY'
import pathlib, re, sys
text = pathlib.Path(sys.argv[1]).read_text()
label = sys.argv[2]
marker = re.search(r"^# Historical archive$", text, re.MULTILINE)
if not marker:
raise SystemExit(f"{label}: missing Historical archive boundary")
current = text[:marker.start()]
if not re.search(r"Internal Qdrant \+ Ollama semantic infrastructure", current, re.MULTILINE):
raise SystemExit(f"{label}: current section missing internal semantic snapshot heading")
if not re.search(r"\b(one|single)\b.*\bworkspace\b.*\b(one|single)\b.*\bQdrant\b.*\bcollection\b", current, re.IGNORECASE | re.DOTALL):
raise SystemExit(f"{label}: current section must describe one-workspace/one-collection ownership")
if not re.search(r"\bDWH\b", current) or not re.search(r"\bLLM\b", current):
raise SystemExit(f"{label}: current section must identify DWH and LLM")
if not re.search(r"\bexternal\b", current, re.IGNORECASE):
raise SystemExit(f"{label}: current section must mark the external boundary")
if "embedding-model-init" not in current:
raise SystemExit(f"{label}: current section missing embedding-model-init")
forbidden = [
r"supported Compose stack is exactly `frontend` plus `core`",
r"DWH, vector DB, embedding, LLM",
r"vector DB, embedding, and LLM remain external",
]
for pattern in forbidden:
if re.search(pattern, current, re.MULTILINE):
raise SystemExit(f"{label}: current section still contains contradictory active text: {pattern}")
PY
}
verify_internal_semantic_infrastructure_docs() {
local readme="$root/README.md"
local agents="$root/AGENTS.md"
local local_manual="$root/docs/install/local-workspace-registry.md"
local server_manual="$root/docs/install/server-workspace-registry.md"
local compact_manual="$root/docs/installazione-docker-4-contesti.md"
local diagnostics="$root/docs/workspace-diagnostic-protocol.md"
local memory="$root/docs/gestione-memory.md"
local secrets="$root/deploy/secrets/README.md"
verify_compose_internal_semantic_contract || return 1
verify_workspace_descriptor_semantic_contract "$root/deploy/workspaces/example.yaml" "example workspace" || return 1
verify_workspace_descriptor_semantic_contract "$root/deploy/workspaces/psd.yaml.example" "psd workspace example" || return 1
verify_vector_helper_interfaces || return 1
verify_project_state_current_contract "$root/PROJECT_STATE.md" "PROJECT_STATE.md" || return 1
verify_workspace_descriptor_doc_contract "$readme" "README" || return 1
verify_workspace_descriptor_doc_contract "$local_manual" "local workspace manual" || return 1
verify_workspace_descriptor_doc_contract "$server_manual" "server workspace manual" || return 1
verify_workspace_descriptor_doc_contract "$diagnostics" "workspace diagnostic protocol" || return 1
local ownership_spec semantic_index_spec compact_spec
ownership_spec='{"rows":[
{"component":"^DWH$","ownership":"^External$","operator contract":"external|endpoint|installation"},
{"component":"^LLM$","ownership":"^External$","operator contract":"external|endpoint|policy"},
{"component":"^Qdrant$","ownership":"^Internal$","operator contract":"internal|Compose|qdrant-data"},
{"component":"^Ollama embedding$","ownership":"^Internal$","operator contract":"internal|Compose|qwen3-embedding:0\\.6b"}
]}'
semantic_index_spec="$(semantic_index_relationship_spec)"
compact_spec='{"rows":[
{"componente":"^DWH$","ownership":"^Esterno$","contratto operativo":"endpoint.*estern"},
{"componente":"^LLM$","ownership":"^Esterno$","contratto operativo":"esterna|esterno"},
{"componente":"^Qdrant$","ownership":"^Interno$","contratto operativo":"interno.*obbligatorio.*qdrant-data"},
{"componente":"^Ollama embedding$","ownership":"^Interno$","contratto operativo":"interno.*obbligatorio.*qwen3-embedding:0\\.6b"}
]}'
verify_markdown_table_relationships "$local_manual" "local workspace manual" "Architecture ownership contract" "$ownership_spec" || return 1
verify_markdown_table_relationships "$server_manual" "server workspace manual" "Architecture ownership contract" "$ownership_spec" || return 1
verify_markdown_table_relationships "$local_manual" "local workspace manual" "Semantic index ownership contract" "$semantic_index_spec" || return 1
verify_markdown_table_relationships "$server_manual" "server workspace manual" "Semantic index ownership contract" "$semantic_index_spec" || return 1
verify_markdown_table_relationships "$compact_manual" "four-context install note" "Contratto sintetico di ownership" "$compact_spec" || return 1
require_pattern "$readme" "README" 'mandatory stack.+qdrant.+embedding.+embedding-model-init' || return 1
require_pattern "$readme" "README" 'qwen3-embedding:0\.6b' || return 1
require_pattern "$readme" "README" 'qdrant-data.+embedding-models' || return 1
require_pattern "$readme" "README" 'confirm-project' || return 1
require_pattern "$agents" "AGENTS.md" 'Qdrant and Ollama are internal Compose services' || return 1
require_pattern "$agents" "AGENTS.md" 'DWH and LLM remain external configuration endpoints' || return 1
for manual in "$local_manual" "$server_manual"; do
require_pattern "$manual" "$(basename "$manual")" 'qwen3-embedding:0\.6b' || return 1
done
require_pattern "$local_manual" "local workspace manual" 'CPU-first' || return 1
require_pattern "$local_manual" "local workspace manual" 'THOTH_ENABLE_EMBEDDING_GPU=1' || return 1
require_pattern "$server_manual" "server workspace manual" 'Qdrant backup/restore' || return 1
require_pattern "$compact_manual" "four-context install note" '1024 dimensioni' || return 1
require_pattern "$diagnostics" "workspace diagnostic protocol" 'semantic_index_incompatible' || return 1
require_absent "$diagnostics" "workspace diagnostic protocol" \
'engine: pgvector' \
'provider: ollama_compatible' \
'THT_WS_<NAMESPACE>_VECTOR_TRANSPORT' \
'THT_WS_<NAMESPACE>_EMBEDDING_BASE_URL' || return 1
require_pattern "$memory" "memory guide" 'Indice Qdrant' || return 1
require_pattern "$memory" "memory guide" 'indice derivato ma persistente' || return 1
require_pattern "$memory" "memory guide" '`kind`' || return 1
require_absent "$memory" "memory guide" \
'Indice pgvector' \
"all'indice pgvector" || return 1
require_pattern "$secrets" "deploy secrets guide" 'THT_MODEL_API_KEY.+THT_DWH_API_KEY.+THT_CA.+THT_SSL_CA' || return 1
require_pattern "$secrets" "deploy secrets guide" 'Do not add vector or embedding endpoint credentials to the bundle' || return 1
require_absent "$secrets" "deploy secrets guide" 'PI_PROVIDER_API_KEY' || return 1
}
verify_local_guide() {
local guide="$root/docs/install/local.md"
[[ -f "$guide" ]] || {
echo "missing local installation guide: docs/install/local.md" >&2
return 1
}
require_headings "$guide" "local installation guide" \
"Choose your platform" \
"Prerequisites" \
"Clone and verify LF" \
"Create the local operator files" \
"Address external services" \
"Build ThothII and tht" \
"Start and verify" \
"Update an installation" \
"Back up and restore" \
"Data-preserving uninstall" \
"Next: workspaces and Pi"
require_text "$guide" "local installation guide" \
"git clone" \
"bash scripts/verify-line-endings.sh" \
"deploy/env/local.env" \
"host.docker.internal" \
"host-gateway" \
"container 127.0.0.1" \
"bash scripts/build-local.sh" \
"scripts/build-local.ps1" \
"bash scripts/build-tht.sh" \
"tht --installation" \
"curl --fail http://127.0.0.1:8080/health" \
"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}`);
}
}
function requirePattern(label, text, pattern) {
if (!pattern.test(text)) throw new Error(label);
}
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", "set -euo pipefail",
"git status --porcelain --untracked-files=all", "USES_BASE_CORE", "thothii-core:local",
]);
if (/\|\|\s*true|;\s*true\b/.test(updateShell)) throw new Error("POSIX source update contains a failure-bypass command");
requirePattern("POSIX source update does not fail closed: source pull", updateShell,
/if ! git pull --ff-only; then abort_update/);
requirePattern("POSIX source update does not fail closed: installation status", updateShell,
/if ! INSTALLATION_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" status\)"; then/);
requirePattern("POSIX source update does not fail closed: Pi status", updateShell,
/if ! RUNNING_PI_VERSION="\$\("\$THT_BIN" --installation "\$INSTALLATION" pi status\)"; then/);
requirePattern("POSIX source update does not fail closed: local build", updateShell,
/if ! bash scripts\/build-local\.sh; then/);
requirePattern("POSIX source update does not fail closed: tht build", updateShell,
/if ! bash scripts\/build-tht\.sh; then/);
requirePattern("POSIX source update lacks the same-version/no-selector path", updateShell,
/if \[\[ "\$NEXT_PI_VERSION" == "\$RUNNING_PI_VERSION" \]\]; then[\s\S]*"\$USES_BASE_CORE" == true[\s\S]*TRANSACTIONAL_PI_UPDATE=false/);
for (const [label, pattern] of [
["installation start", /if ! "\$THT_BIN" --installation "\$INSTALLATION" start; then/],
["frontend health", /if ! curl --fail http:\/\/127\.0\.0\.1:8080\/health; then/],
["core health", /if ! curl --fail http:\/\/127\.0\.0\.1:8787\/health; then/],
["final status", /if ! FINAL_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" status\)"; then/],
["final Pi status", /if ! FINAL_PI_STATUS="\$\("\$THT_BIN" --installation "\$INSTALLATION" pi status\)"; then/],
["final doctor", /if ! "\$THT_BIN" --installation "\$INSTALLATION" doctor; then/],
]) requirePattern(`POSIX source update does not fail closed: ${label}`, updateShell, pattern);
const provenance = updateShell.indexOf("printf 'Built source revision:");
if (provenance < updateShell.lastIndexOf("require_clean_source") ||
provenance < updateShell.indexOf('abort_update "final doctor failed"')) {
throw new Error("POSIX source revision provenance is printed before final checks");
}
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", "$ErrorActionPreference = 'Stop'",
"git status --porcelain --untracked-files=all", "$UsesBaseCore", "thothii-core:local",
"$TransactionalPiUpdate = $false",
]);
for (const [command, step] of [
["git pull --ff-only", "source pull"],
["$InstallationStatus = @(& $THT_BIN --installation $INSTALLATION status)", "installation status"],
["$RunningPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)", "Pi status"],
["powershell -ExecutionPolicy Bypass -File scripts/build-local.ps1", "local image build"],
["& \"C:\\Program Files\\Git\\bin\\bash.exe\" scripts/build-tht.sh", "tht build"],
["curl.exe --fail --silent --show-error http://127.0.0.1:8080/health", "frontend health check"],
["curl.exe --fail --silent --show-error http://127.0.0.1:8787/health", "core health check"],
["$FinalPiStatus = (& $THT_BIN --installation $INSTALLATION pi status)", "final Pi status"],
["& $THT_BIN --installation $INSTALLATION doctor", "final doctor"],
]) {
const commandAt = updatePowerShell.indexOf(command);
const checkAt = updatePowerShell.indexOf(`Assert-NativeSuccess '${step}'`, commandAt);
if (commandAt < 0 || checkAt < commandAt || checkAt - commandAt > 220) {
throw new Error(`PowerShell source update does not propagate failure: ${step}`);
}
}
requirePattern("PowerShell source update lacks the same-version/no-selector path", updatePowerShell,
/if \(\$NextPiVersion -eq \$RunningPiVersion\) \{[\s\S]*-not \$UsesBaseCore[\s\S]*\$TransactionalPiUpdate = \$false/);
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
local update_fixture update_script fake_bin calls output status
update_fixture="$(mktemp -d "${tmp_prefix}thoth-source-update.XXXXXX")"
trap 'rm -rf "$update_fixture"' RETURN
update_script="$update_fixture/update.sh"
awk '
/^## Update an installation$/ { in_section=1; next }
in_section && /^```sh$/ { in_code=1; next }
in_code && /^```$/ { exit }
in_code { print }
' "$guide" >"$update_script"
chmod 0700 "$update_script"
mkdir -p "$update_fixture/project/docker" "$update_fixture/project/scripts" "$update_fixture/bin"
printf 'ARG PI_VERSION=0.80.3\n' >"$update_fixture/project/docker/core.Dockerfile"
printf '%s\n' \
'#!/bin/sh' \
'printf "git %s\n" "$*" >>"$CALLS"' \
'case "$1" in' \
' status) if [ "$FAIL_STEP" = dirty ]; then printf "?? untracked-build-context\n"; fi ;;' \
' pull) [ "$FAIL_STEP" != pull ] || exit 9 ;;' \
' rev-parse) printf "0123456789abcdef\n" ;;' \
'esac' \
'exit 0' >"$update_fixture/bin/git"
printf '%s\n' \
'#!/bin/sh' \
'printf "bash %s\n" "$*" >>"$CALLS"' \
'if [ "$1" = scripts/build-local.sh ] && [ "$FAIL_STEP" = build ]; then exit 8; fi' \
'exit 0' >"$update_fixture/bin/bash"
printf '%s\n' \
'#!/bin/sh' \
'printf "tht %s\n" "$*" >>"$CALLS"' \
'case " $* " in' \
' *" pi status "*) [ "$FAIL_STEP" != status ] || exit 7; printf "Pi version: 0.80.3\n" ;;' \
' *" status "*) printf "[{\"Service\":\"core\",\"Image\":\"thothii-core:local\"}]\n" ;;' \
'esac' \
'exit 0' >"$update_fixture/bin/tht"
printf '%s\n' \
'#!/bin/sh' \
'printf "curl %s\n" "$*" >>"$CALLS"' \
'exit 0' >"$update_fixture/bin/curl"
chmod 0700 "$update_fixture/bin/git" "$update_fixture/bin/bash" \
"$update_fixture/bin/tht" "$update_fixture/bin/curl"
for fixture_step in clean dirty pull status build; do
calls="$update_fixture/calls-$fixture_step"
output="$update_fixture/output-$fixture_step"
: >"$calls"
set +e
(
cd "$update_fixture/project"
env PATH="$update_fixture/bin:$PATH" CALLS="$calls" FAIL_STEP="$fixture_step" \
THT_BIN="$update_fixture/bin/tht" INSTALLATION="$update_fixture/installation.yaml" \
/bin/bash "$update_script"
) >"$output" 2>&1
status=$?
set -e
if [[ "$fixture_step" == clean ]]; then
[[ $status -eq 0 ]] || { echo "same-version/no-selector source fixture failed" >&2; return 1; }
grep -Fq 'Built source revision: 0123456789abcdef' "$output" || {
echo "successful source fixture did not report revision provenance" >&2; return 1;
}
if grep -Fq ' pi update ' "$calls"; then
echo "same-version/no-selector source fixture incorrectly invoked pi update" >&2
return 1
fi
grep -Fq 'bash scripts/build-local.sh' "$calls" || return 1
grep -Fq 'tht --installation ' "$calls" || return 1
else
[[ $status -ne 0 ]] || { echo "$fixture_step source failure fixture was accepted" >&2; return 1; }
if grep -Fq 'Built source revision:' "$output"; then
echo "$fixture_step source failure fixture claimed revision provenance" >&2
return 1
fi
fi
done
echo "source update fail-closed semantics passed"
echo "local installation guide contract passed"
}
verify_windows_line_endings_guide() {
local guide="$root/docs/install/windows-line-endings.md"
[[ -f "$guide" ]] || {
echo "missing Windows line-ending guide: docs/install/windows-line-endings.md" >&2
return 1
}
require_headings "$guide" "Windows line-ending guide" \
"Recommended WSL2 clone" \
"Repository-local LF policy" \
"Verify after clone or pull" \
"Recover an existing CRLF clone"
require_text "$guide" "Windows line-ending 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"
node - "$guide" <<'NODE'
const fs = require("fs");
const source = fs.readFileSync(process.argv[2], "utf8");
const lines = source.split(/\n/);
const sectionStart = source.indexOf("## Recover an existing CRLF clone");
const recovery = source.slice(sectionStart);
const shell = [...recovery.matchAll(/```sh\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
const powershell = [...recovery.matchAll(/```powershell\n([\s\S]*?)```/g)].map((match) => match[1]).join("\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 (const token of [
"set -euo pipefail", "validate_index_export", "validate_worktree_modes", "rewrite_index_entry", "git ls-files -s -z",
"100644", "100755", "120000", "readlink", "ln -s", "if ! git checkout-index",
]) {
if (!shell.includes(token)) throw new Error(`POSIX CRLF repair lacks fail-closed semantic: ${token}`);
}
if (/\|\|\s*true|;\s*true\b/.test(shell)) throw new Error("POSIX CRLF repair contains a failure-bypass command");
const exportAt = shell.indexOf("if ! git checkout-index");
const validationAt = shell.indexOf("if ! validate_index_export", exportAt);
const exportedBytesAt = shell.indexOf('if ! bash scripts/verify-line-endings.sh "$REPAIR_DIR"', validationAt);
const rewriteAt = shell.indexOf("if ! git ls-files -s -z", exportedBytesAt);
const finalModesAt = shell.indexOf("if ! validate_worktree_modes; then", rewriteAt);
const finalAt = shell.indexOf("if ! bash scripts/verify-line-endings.sh; then", finalModesAt);
if ([exportAt, validationAt, exportedBytesAt, rewriteAt, finalModesAt, finalAt].some((index) => index < 0) ||
!(exportAt < validationAt && validationAt < exportedBytesAt && exportedBytesAt < rewriteAt && rewriteAt < finalModesAt && finalModesAt < finalAt)) {
throw new Error("POSIX CRLF repair does not prove a complete export before destructive rewrite");
}
for (const token of [
"$ErrorActionPreference = 'Stop'", "Assert-NativeSuccess 'index renormalization'",
"Assert-NativeSuccess 'normalized index check'", "Assert-NativeSuccess 'index export'",
"Assert-NativeSuccess 'index inventory'", "100644", "100755", "120000", "SymbolicLink",
"-ErrorAction Stop", "$WorktreeItem", "Assert-NativeSuccess 'repaired worktree LF verification'",
]) {
if (!powershell.includes(token)) throw new Error(`PowerShell CRLF repair lacks failure propagation: ${token}`);
}
const warningPattern = /WARNING[^\n]*destructive[^\n]*(backup|commit)/i;
const powerShellWarningAt = powershell.indexOf("# WARNING: destructive copy");
const powerShellRewriteAt = powershell.indexOf("foreach ($Entry in $IndexEntries)", powerShellWarningAt);
if (!warningPattern.test(shell.slice(Math.max(0, rewriteAt - 180), rewriteAt)) ||
powerShellRewriteAt < 0 ||
!warningPattern.test(powershell.slice(Math.max(0, powerShellRewriteAt - 180), powerShellRewriteAt))) {
throw new Error("worktree rewrite lacks an immediate destructive warning requiring backup/commit");
}
NODE
local repair_root repair_script real_git partial_repo clean_repo partial_output repair_status
repair_root="$(mktemp -d "${tmp_prefix}thoth-crlf-repair.XXXXXX")"
trap 'rm -rf "$repair_root"' RETURN
repair_script="$repair_root/repair.sh"
awk '
/^## Recover an existing CRLF clone$/ { in_section=1; next }
in_section && /^```sh$/ { in_code=1; next }
in_code && /^```$/ { exit }
in_code { print }
' "$guide" >"$repair_script"
chmod 0700 "$repair_script"
prepare_crlf_fixture() {
local repository="$1"
mkdir -p "$repository/scripts"
git -C "$repository" init -q
printf '*.sh text eol=lf\n' >"$repository/.gitattributes"
printf '#!/bin/sh\nexit 0\n' >"$repository/repair.sh"
printf 'target\n' >"$repository/target.txt"
cp "$root/scripts/verify-line-endings.sh" "$repository/scripts/verify-line-endings.sh"
ln -s target.txt "$repository/workspace-link"
git -C "$repository" add .gitattributes repair.sh target.txt workspace-link \
scripts/verify-line-endings.sh 2>/dev/null
printf '#!/bin/sh\r\nexit 0\r\n' >"$repository/repair.sh"
}
partial_repo="$repair_root/partial/worktree"
mkdir -p "$partial_repo" "$repair_root/partial/bin"
prepare_crlf_fixture "$partial_repo"
real_git="$(command -v git)"
printf '%s\n' \
'#!/bin/sh' \
'"$REAL_GIT" "$@"' \
'status=$?' \
'if [ $status -eq 0 ] && [ "$1" = checkout-index ]; then rm -f "$PARTIAL_EXPORT_PATH"; fi' \
'exit $status' >"$repair_root/partial/bin/git"
chmod 0700 "$repair_root/partial/bin/git"
partial_output="$repair_root/partial/output"
set +e
(
cd "$partial_repo"
env PATH="$repair_root/partial/bin:$PATH" REAL_GIT="$real_git" \
PARTIAL_EXPORT_PATH="$repair_root/partial/ThothII-lf-repair/repair.sh" \
/bin/bash "$repair_script"
) >"$partial_output" 2>&1
repair_status=$?
set -e
[[ $repair_status -ne 0 ]] || { echo "partial CRLF export fixture was accepted" >&2; return 1; }
LC_ALL=C grep -q $'\r' "$partial_repo/repair.sh" || {
echo "partial CRLF export fixture rewrote bytes before complete validation" >&2; return 1;
}
[[ -L "$partial_repo/workspace-link" && "$(readlink "$partial_repo/workspace-link")" == target.txt ]] || {
echo "partial CRLF export fixture changed the tracked symlink" >&2; return 1;
}
clean_repo="$repair_root/clean/worktree"
mkdir -p "$clean_repo"
prepare_crlf_fixture "$clean_repo"
(cd "$clean_repo" && /bin/bash "$repair_script") >/dev/null
"$root/scripts/verify-line-endings.sh" "$clean_repo"
[[ -L "$clean_repo/workspace-link" && "$(readlink "$clean_repo/workspace-link")" == target.txt ]] || {
echo "successful CRLF repair did not preserve the mode-120000 symlink" >&2; return 1;
}
echo "CRLF recovery rewrites bytes and preserves mode-120000 symlinks passed"
echo "Windows line-ending recovery guide contract passed"
}
verify_pi_management_guide() {
local guide="$root/docs/install/pi-management.md"
local lifecycle_contract="$root/docs/contracts/tht-pi.md"
[[ -f "$guide" ]] || {
echo "missing Pi management guide: docs/install/pi-management.md" >&2
return 1
}
[[ -f "$lifecycle_contract" ]] || {
echo "missing Pi lifecycle contract: docs/contracts/tht-pi.md" >&2
return 1
}
require_text "$lifecycle_contract" "Pi lifecycle contract" \
"io.thothii.pi.version"
if grep -Fq 'org.opencontainers.image.version' "$lifecycle_contract"; then
echo "Pi lifecycle contract must use io.thothii.pi.version, not org.opencontainers.image.version" >&2
return 1
fi
require_headings "$guide" "Pi management guide" \
"Choose application defaults" \
"Edit the provider catalog and enabled-model policy" \
"Store provider credentials" \
"Reload changed configuration" \
"Update the bundled Pi version" \
"Recover a failed lifecycle operation" \
"Direct support access"
require_text "$guide" "Pi management guide" \
"pi status" \
"pi doctor" \
"pi test" \
"pi check" \
"pi configure" \
"pi restart --yes --drain" \
"restart only core" \
"deploy/pi/models.json" \
"deploy/pi/settings.json" \
"policy only" \
"backend installation settings" \
"PI_AUTH_FILE" \
"pi update" \
"pi rollback --yes" \
"pi maintenance status" \
"pi maintenance recover --yes" \
"pi logs" \
"/run/secrets" \
"Raw Compose access is unsupported"
if grep -Fq '~/.pi/agent/' "$guide"; then
echo "Pi management guide must not direct ThothII operators to native Pi paths" >&2
return 1
fi
if grep -Eqi 'browser shell|host-native Pi|host Pi|running container|live container' "$guide"; then
echo "Pi management guide must not include native-Pi, browser-shell, or live-container workflow" >&2
return 1
fi
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", "tht", "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"
}
verify_server_guide() {
local guide="$root/docs/install/server.md"
[[ -f "$guide" ]] || {
echo "missing server installation guide sections: docs/install/server.md" >&2
return 1
}
require_headings "$guide" "server installation guide" \
"Deployment contract" \
"Service account and directories" \
"Firewall and network boundaries" \
"Address co-resident external services" \
"Prepare operator files and secrets" \
"Build locally or select pinned images" \
"Install tht" \
"Start and verify readiness" \
"Configure TLS and upstream authentication" \
"Operate Pi, drain, and roll back" \
"Back up and restore" \
"Diagnostics" \
"Data-preserving uninstall"
require_text "$guide" "server installation guide" \
"frontend" \
"core" \
"UID/GID 10001" \
"does not require or permit creation" \
"getent passwd 10001" \
"getent group 10001" \
"-m 0750 /srv/thothii/operator" \
"chmod 0600 /srv/thothii/operator/server.env" \
"THT_THT_OUTPUT_DIRECTORY=/srv/thothii/operator/build-output" \
"/srv/thothii" \
"example operator root" \
"/run/secrets" \
"Git-backed workspace registry is the source of truth" \
"host.docker.internal" \
"host-gateway" \
"com.docker.network.bridge.name" \
"DOCKER-USER" \
"iptables -I INPUT" \
"container 127.0.0.1" \
"collection" \
"embedding" \
"bash scripts/build-local.sh" \
"@sha256:" \
"bash scripts/build-tht.sh" \
"tht --installation" \
"sessions migrate --yes" \
'"pending":[]' \
'"drifted":[]' \
"remove --yes" \
"THT_BACKUP_ROOT=/srv/thothii-backups" \
"sha256sum --check SHA256SUMS" \
"curl --fail http://127.0.0.1:8080/health" \
"https://thoth.example.com" \
"pi update" \
"--drain" \
"pi rollback --yes" \
"pi maintenance recover --yes" \
"docker compose down --volumes" \
"reverse-proxy-nginx.md" \
"reverse-proxy-caddy.md"
if ! grep -Fq 'sudo install -d -o "$operator_uid" -g 10001 -m 0750 /srv/thothii' "$guide"; then
echo "server installation guide does not set parent traversal boundary" >&2
return 1
fi
if grep -Eq '(^|[[:space:]])(sudo[[:space:]]+)?(useradd|groupadd|usermod)([[:space:]]|$)|sudo[[:space:]]+-u[[:space:]]+thothii|thothii-ops' "$guide"; then
echo "server installation guide creates or depends on a host identity" >&2
return 1
fi
node - "$guide" <<'NODE'
const fs = require("fs");
const source = fs.readFileSync(process.argv[2], "utf8");
if (/omics_portal|chirone|localllm_default|datamart-builder|compose\.production|compose\.psd-local/i.test(source)) {
throw new Error("server installation guide introduces forbidden application coupling");
}
if (/\/var\/run\/docker\.sock|docker\.sock/i.test(source)) {
throw new Error("server installation guide introduces a Docker socket dependency");
}
for (const line of source.split(/\n/)) {
const match = line.match(/^\s*([A-Z][A-Z0-9_]*(?:PASSWORD|TOKEN|API_KEY|SECRET)[A-Z0-9_]*)\s*=\s*(\S.*)$/);
if (!match) continue;
const [, name, rawValue] = match;
const value = rawValue.trim();
if (!/(?:_FILE|_SOURCE)$/.test(name) && value && !/^\$\{?[A-Z_][A-Z0-9_]*\}?$/.test(value)) {
throw new Error("server installation guide embeds a secret value");
}
}
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("server docker compose down --volumes must appear only in an explicit prose prohibition");
}
}
if (/```(?:sh|bash)\n[\s\S]*?\bdocker\s+rm\b[\s\S]*?```/i.test(source)) {
throw new Error("server uninstall bypasses installation-aware removal");
}
if (/host-gateway[^\n]{0,120}(?:listen|listening|bound)[^\n]{0,80}127\.0\.0\.1|(?:listen|listening|bound)[^\n]{0,80}127\.0\.0\.1[^\n]{0,120}host-gateway/i.test(source)) {
throw new Error("server host-gateway guidance assumes a host loopback listener");
}
const pinnedStart = source.indexOf("## Build locally or select pinned images");
const pinnedEnd = source.indexOf("\n## ", pinnedStart + 3);
const pinnedSection = source.slice(pinnedStart, pinnedEnd < 0 ? source.length : pinnedEnd);
const pinnedBlock = [...pinnedSection.matchAll(/```yaml\n([\s\S]*?)```/g)].map((match) => match[1])
.find((block) => block.includes("session-migrate:")) || "";
function pinnedService(name) {
const match = pinnedBlock.match(new RegExp(`^ ${name}:\\n((?: [^\\n]*\\n)+)`, "m"));
return match ? match[1] : "";
}
const pinnedCore = pinnedService("core");
const pinnedMigrator = pinnedService("session-migrate");
const pinnedFrontend = pinnedService("frontend");
const coreImage = pinnedCore.match(/image:\s*(\S+)/)?.[1];
const migratorImage = pinnedMigrator.match(/image:\s*(\S+)/)?.[1];
const frontendImage = pinnedFrontend.match(/image:\s*(\S+)/)?.[1];
if (![pinnedCore, pinnedMigrator, pinnedFrontend].every((block) => block.includes("build: !reset null")) ||
!coreImage || coreImage !== migratorImage || !/@sha256:<64-lowercase-hex-digits>$/.test(coreImage) ||
!frontendImage || !/@sha256:<64-lowercase-hex-digits>$/.test(frontendImage)) {
throw new Error("server pinned image override must pin core, session-migrate, and frontend without builds");
}
if (/session-migrate:[\s\S]{0,180}image:\s*thothii-core:local/.test(source) &&
/core:[\s\S]{0,180}image:\s*registry\.[^\n]+@sha256:[a-f0-9]{64}/.test(source)) {
throw new Error("server pinned migration image must equal the pinned core image");
}
if (/```(?:sh|bash)\n[\s\S]*?\bdocker compose\s+(?:up|stop|down|restart|pull|build)\b[\s\S]*?```/i.test(source)) {
throw new Error("server lifecycle must use tht, not raw Docker Compose");
}
NODE
echo "server installation guide contract passed"
}
verify_reverse_proxy_nginx_guide() {
local guide="$root/docs/install/reverse-proxy-nginx.md"
[[ -f "$guide" ]] || {
echo "missing Nginx reverse-proxy guide: docs/install/reverse-proxy-nginx.md" >&2
return 1
}
require_headings "$guide" "Nginx reverse-proxy guide" \
"Trust boundary" \
"Validate and reload" \
"Test authentication and SSE"
require_text "$guide" "Nginx reverse-proxy guide" \
"Forwarding identity headers alone does not authenticate a user" \
"authentication gateway" \
"2xx" \
"TLS" \
"frontend"
node - "$guide" <<'NODE'
const fs = require("fs");
const source = fs.readFileSync(process.argv[2], "utf8");
const block = [...source.matchAll(/```nginx\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
function stripNginxComments(text) {
let effective = "";
let quote = null;
let escaped = false;
let comment = false;
for (const character of text) {
if (comment) {
if (character === "\n") {
effective += character;
comment = false;
}
continue;
}
if (escaped) {
effective += character;
escaped = false;
continue;
}
if (character === "\\") {
effective += character;
escaped = true;
continue;
}
if (quote !== null) {
effective += character;
if (character === quote) quote = null;
continue;
}
if (character === '"' || character === "'") {
effective += character;
quote = character;
continue;
}
if (character === "#") {
comment = true;
continue;
}
effective += character;
}
return effective;
}
const effectiveBlock = stripNginxComments(block);
const tokens = [
"listen 443 ssl;", "ssl_certificate ", "ssl_certificate_key ",
"location = /_authenticate {", "internal;", "proxy_pass http://auth-gateway:4180/verify;",
"auth_request /_authenticate;", "proxy_pass http://127.0.0.1:8080;",
"proxy_http_version 1.1;", "proxy_buffering off;", "proxy_cache off;",
"proxy_read_timeout 3600s;",
];
if (/127\.0\.0\.1:8787|\bcore:8787\b/.test(effectiveBlock) || !effectiveBlock.includes("http://127.0.0.1:8080")) {
throw new Error("Nginx proxy must forward only to frontend on 127.0.0.1:8080");
}
for (const token of tokens) {
if (!effectiveBlock.includes(token)) throw new Error(`Nginx proxy lacks structural token: ${token}`);
}
if (/proxy_set_header\s+X-Thoth-Trusted-[^;]+\$http_/i.test(effectiveBlock)) {
throw new Error("Nginx proxy trusts a client-supplied identity header");
}
const identities = [
["issuer", "Principal-Issuer", "thoth_principal_issuer", "x_thoth_principal_issuer"],
["subject", "Principal-Subject", "thoth_principal_subject", "x_thoth_principal_subject"],
["display", "Principal-Display-Name", "thoth_principal_display_name", "x_thoth_principal_display_name"],
["admin", "Is-Admin", "thoth_is_admin", "x_thoth_is_admin"],
];
function escaped(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
function nginxLocations(text) {
const locations = [];
const pattern = /\blocation\s+([^\n{]+)\{/g;
for (const match of text.matchAll(pattern)) {
const opening = match.index + match[0].lastIndexOf("{");
let depth = 0;
let closing = -1;
for (let index = opening; index < text.length; index++) {
if (text[index] === "{") depth++;
if (text[index] === "}" && --depth === 0) {
closing = index;
break;
}
}
if (closing < 0) throw new Error(`Nginx proxy has unterminated location: ${match[1].trim()}`);
locations.push({selector: match[1].trim(), body: text.slice(opening + 1, closing)});
}
return locations;
}
const locations = nginxLocations(effectiveBlock);
const frontendLocations = locations.filter((location) =>
/proxy_pass\s+http:\/\/127\.0\.0\.1:8080\s*;/.test(location.body));
if (frontendLocations.length === 0) {
throw new Error("Nginx proxy lacks a frontend upstream location");
}
const authenticatedFrontendLocations = frontendLocations.filter((location) =>
/auth_request\s+\/_authenticate\s*;/.test(location.body));
const directFrontendLocations = frontendLocations.filter((location) =>
!/auth_request\s+\/_authenticate\s*;/.test(location.body));
if (directFrontendLocations.length > 1) {
throw new Error("Nginx direct OIDC mode contains an additional frontend bypass location");
}
for (const frontendLocation of frontendLocations) {
for (const token of [
"proxy_http_version 1.1;", "proxy_buffering off;", "proxy_cache off;",
"proxy_read_timeout 3600s;",
]) {
if (!frontendLocation.body.includes(token)) {
throw new Error(`Nginx proxy lacks structural token: ${token}`);
}
}
}
if (authenticatedFrontendLocations.length > 0) {
const authLocations = locations.filter((location) => location.selector === "= /_authenticate");
if (authLocations.length !== 1) {
throw new Error("Nginx proxy must define exactly one authentication location for upstream mode");
}
const authLocation = authLocations[0].body;
for (const [label, publicName, variable, upstream] of identities) {
const trustedName = publicName === "Is-Admin" ? "Is-Admin" : publicName;
const publicClear = new RegExp(`proxy_set_header\\s+X-Thoth-${escaped(publicName)}\\s+"";`);
const trustedHeader = `X-Thoth-Trusted-${trustedName}`;
const trustedClear = new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+"";`);
const authPublicAt = authLocation.search(publicClear);
const authTrustedAt = authLocation.search(trustedClear);
if (authPublicAt < 0) {
throw new Error(`Nginx auth location does not clear inbound ${label} identity`);
}
if (authTrustedAt < 0) {
throw new Error(`Nginx auth location does not clear inbound trusted ${label} identity`);
}
for (const frontendLocation of authenticatedFrontendLocations) {
const frontendPublicAt = frontendLocation.body.search(publicClear);
if (frontendPublicAt < 0) {
throw new Error(`Nginx frontend location does not clear inbound ${label} identity`);
}
const normalizedFrontend = frontendLocation.body.replace(/\s+/g, " ");
const captureAt = normalizedFrontend.search(new RegExp(`auth_request_set\\s+\\$${variable}\\s+\\$upstream_http_${upstream};`));
if (captureAt < 0) {
throw new Error(`Nginx frontend location does not capture authenticated ${label} identity`);
}
const mapAt = normalizedFrontend.search(new RegExp(`proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`));
if (mapAt < 0) {
throw new Error(`Nginx frontend location does not map authenticated ${label} identity`);
}
}
if (new RegExp(`auth_request_set\\s+\\$${variable}|proxy_set_header\\s+${escaped(trustedHeader)}\\s+\\$${variable};`).test(authLocation)) {
throw new Error(`Nginx auth location performs a forbidden ${label} capture or mapping`);
}
}
} else if (directFrontendLocations.length === 0) {
throw new Error("Nginx proxy lacks a direct or authenticated frontend path");
}
for (const location of locations) {
const upstreams = [...location.body.matchAll(/proxy_pass\s+([^;]+);/g)].map((match) => match[1].trim());
for (const upstream of upstreams) {
if (location.selector === "= /_authenticate" && upstream === "http://auth-gateway:4180/verify") continue;
if (upstream === "http://127.0.0.1:8080") continue;
throw new Error(`Nginx location proxies to an unreviewed upstream: ${upstream}`);
}
}
for (const frontendLocation of authenticatedFrontendLocations) {
if (!/auth_request\s+\/_authenticate\s*;/.test(frontendLocation.body)) {
throw new Error("Nginx authenticated frontend path bypasses complete authentication contract");
}
}
NODE
echo "Nginx reverse-proxy guide contract passed"
}
verify_reverse_proxy_caddy_guide() {
local guide="$root/docs/install/reverse-proxy-caddy.md"
[[ -f "$guide" ]] || {
echo "missing Caddy reverse-proxy guide: docs/install/reverse-proxy-caddy.md" >&2
return 1
}
require_headings "$guide" "Caddy reverse-proxy guide" \
"Trust boundary" \
"Validate and reload" \
"Test authentication and SSE"
require_text "$guide" "Caddy reverse-proxy guide" \
"Forwarding identity headers alone does not authenticate a user" \
"authentication gateway" \
"2xx" \
"Caddy terminates TLS" \
"frontend"
node - "$guide" <<'NODE'
const fs = require("fs");
const source = fs.readFileSync(process.argv[2], "utf8");
const block = [...source.matchAll(/```caddyfile\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
const tokens = [
"thoth.example.invalid {", "route {",
"forward_auth auth-gateway:4180 {", "uri /verify", "copy_headers {",
"reverse_proxy 127.0.0.1:8080 {", "flush_interval -1",
];
if (/127\.0\.0\.1:8787|\bcore:8787\b/.test(block) || !block.includes("127.0.0.1:8080")) {
throw new Error("Caddy proxy must forward only to frontend on 127.0.0.1:8080");
}
for (const token of tokens) {
if (!block.includes(token)) throw new Error(`Caddy proxy lacks structural token: ${token}`);
}
function directiveBlock(text, marker) {
const start = text.indexOf(marker);
if (start < 0) throw new Error(`Caddy proxy lacks scoped block: ${marker}`);
const opening = text.indexOf("{", start);
let depth = 0;
for (let index = opening; index < text.length; index++) {
if (text[index] === "{") depth++;
if (text[index] === "}" && --depth === 0) return {start, end: index, body: text.slice(opening + 1, index)};
}
throw new Error(`Caddy proxy has unterminated scoped block: ${marker}`);
}
const route = directiveBlock(block, "route {");
const forward = directiveBlock(route.body, "forward_auth auth-gateway:4180 {");
const forwardAt = route.body.indexOf("forward_auth auth-gateway:4180 {");
for (const [label, publicName, trustedName] of [
["issuer", "X-Thoth-Principal-Issuer", "X-Thoth-Trusted-Principal-Issuer"],
["subject", "X-Thoth-Principal-Subject", "X-Thoth-Trusted-Principal-Subject"],
["display", "X-Thoth-Principal-Display-Name", "X-Thoth-Trusted-Principal-Display-Name"],
["admin", "X-Thoth-Is-Admin", "X-Thoth-Trusted-Is-Admin"],
]) {
const publicClearAt = route.body.indexOf(`request_header -${publicName}`);
if (publicClearAt < 0) {
throw new Error(`Caddy proxy does not clear inbound ${label} identity`);
}
const trustedClearAt = route.body.indexOf(`request_header -${trustedName}`);
if (trustedClearAt < 0) {
throw new Error(`Caddy proxy does not clear inbound trusted ${label} identity`);
}
if (publicClearAt > forwardAt || trustedClearAt > forwardAt) {
throw new Error("Caddy identity clears must precede forward_auth");
}
if (!forward.body.includes(`${publicName}>${trustedName}`)) {
throw new Error(`Caddy proxy does not map authenticated ${label} identity`);
}
}
const outsideForward = route.body.slice(0, forward.start) + route.body.slice(forward.end + 1);
if (/X-Thoth-(?:Principal-[^\s>]+|Is-Admin)>X-Thoth-Trusted-/.test(outsideForward)) {
throw new Error("Caddy maps identity outside the authenticated response stage");
}
NODE
echo "Caddy reverse-proxy guide contract passed"
}
verify_caddy_adapted_identity_order() {
local adapted="$1"
node - "$adapted" <<'NODE'
const fs = require("fs");
const document = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const publicHeaders = [
"X-Thoth-Principal-Issuer", "X-Thoth-Principal-Subject",
"X-Thoth-Principal-Display-Name", "X-Thoth-Is-Admin",
];
const trustedHeaders = [
"X-Thoth-Trusted-Principal-Issuer", "X-Thoth-Trusted-Principal-Subject",
"X-Thoth-Trusted-Principal-Display-Name", "X-Thoth-Trusted-Is-Admin",
];
function authUpstream(handler) {
return handler?.handler === "reverse_proxy" &&
(handler.upstreams || []).some((upstream) => upstream.dial === "auth-gateway:4180");
}
function frontendUpstream(handler) {
return handler?.handler === "reverse_proxy" &&
(handler.upstreams || []).some((upstream) => upstream.dial === "127.0.0.1:8080");
}
function collectTrustedSets(value, collected = new Map()) {
if (!value || typeof value !== "object") return collected;
if (value.handler === "headers") {
for (const [name, replacement] of Object.entries(value.request?.set || {})) {
if (trustedHeaders.includes(name)) collected.set(name, replacement);
}
}
for (const child of Object.values(value)) collectTrustedSets(child, collected);
return collected;
}
const expectedClears = [...publicHeaders, ...trustedHeaders];
function validateAuthenticatedMappings(auth) {
const successResponse = (auth.handle_response || []).find((response) =>
(response.match?.status_code || []).map(Number).includes(2));
if (!successResponse) throw new Error("Caddy adapted identity mapping is not restricted to auth 2xx");
const mappings = collectTrustedSets(successResponse);
for (let index = 0; index < trustedHeaders.length; index++) {
const replacement = mappings.get(trustedHeaders[index]);
const expected = `{http.reverse_proxy.header.${publicHeaders[index]}}`;
if (!Array.isArray(replacement) || replacement.length !== 1 || replacement[0] !== expected) {
throw new Error(`Caddy adapted authenticated mapping is invalid for ${trustedHeaders[index]}`);
}
}
}
function validateFrontendPath(handlers) {
let authAt = -1;
for (let index = handlers.length - 1; index >= 0; index--) {
if (authUpstream(handlers[index])) {
authAt = index;
break;
}
}
if (authAt < 0) {
throw new Error("Caddy adapted frontend path bypasses complete authentication contract");
}
for (const header of expectedClears) {
const clearAt = handlers.findIndex((handler) =>
handler?.handler === "headers" && (handler.request?.delete || []).includes(header));
if (clearAt < 0 || clearAt >= authAt) {
throw new Error("Caddy adapted identity clears must execute before authentication");
}
}
validateAuthenticatedMappings(handlers[authAt]);
for (let index = 0; index < handlers.length; index++) {
if (index !== authAt && collectTrustedSets(handlers[index]).size !== 0) {
throw new Error("Caddy adapted config maps trusted identity outside auth success");
}
}
}
let frontendPaths = 0;
function walk(value, inherited = []) {
if (!value || typeof value !== "object") return;
if (Array.isArray(value)) {
for (const child of value) walk(child, inherited);
return;
}
if (frontendUpstream(value)) {
frontendPaths++;
validateFrontendPath(inherited);
}
if (Array.isArray(value.handle)) {
const previous = [];
for (const handler of value.handle) {
walk(handler, [...inherited, ...previous]);
previous.push(handler);
}
for (const [key, child] of Object.entries(value)) {
if (key !== "handle") walk(child, inherited);
}
return;
}
const childContext = authUpstream(value) ? [...inherited, value] : inherited;
for (const child of Object.values(value)) walk(child, childContext);
}
walk(document);
if (frontendPaths === 0) {
throw new Error("Caddy adapted config lacks a frontend handler path");
}
NODE
}
verify_caddy_effective_proxy_guide() {
local adapted
adapted="$(mktemp "${tmp_prefix}thoth-caddy-adapted.XXXXXX")"
if ! awk '
/^```caddyfile$/ { code=1; next }
code && /^```$/ { exit }
code { print }
' "$root/docs/install/reverse-proxy-caddy.md" \
| docker run --rm -i caddy:2.10.2-alpine caddy adapt --config - --adapter caddyfile >"$adapted"; then
rm -f "$adapted"
echo "Caddy documented configuration could not be adapted" >&2
return 1
fi
verify_caddy_adapted_identity_order "$adapted"
rm -f "$adapted"
echo "Caddy adapted trust-stage contract passed"
}
verify_manual() {
local profile="$1" manual
manual="$root/docs/install/$profile-workspace-registry.md"
local -a headings
if [[ "$profile" == local ]]; then
headings=(
"Prerequisites"
"Prepare and publish a workspace source"
"Configure the remote Git repository"
"Start and update the installation"
"Complete runtime secrets in Workspace management"
"Validation and activation behavior"
"Backup, rotation, and recovery"
"Troubleshooting"
)
else
headings=(
"Service account, storage, and firewall"
"Prepare and publish a workspace source"
"Configure the remote Git repository"
"Start and update the installation"
"Complete runtime secrets in Workspace management"
"Validation and activation behavior"
"Backup, rotation, and recovery"
"Troubleshooting"
)
fi
for heading in "${headings[@]}"; do
grep -Fqx "## $heading" "$manual" || {
echo "missing required heading in $profile manual: $heading" >&2
return 1
}
done
local -a expected_steps
if [[ "$profile" == local ]]; then
expected_steps=(
'export THT_SOURCE_ROOT=/absolute/path/to/ThothII'
'thothii-installation.yaml'
'workspaceRepository'
'"$THT_BIN" --installation "$INSTALLATION" start'
'"$THT_BIN" --installation "$INSTALLATION" doctor'
)
else
expected_steps=(
'THT_BIN=/srv/thothii/operator/tht'
'INSTALLATION=/srv/thothii/operator/thothii-installation.yaml'
'"$THT_BIN" --installation "$INSTALLATION" start'
'"$THT_BIN" --installation "$INSTALLATION" doctor'
'docs/install/examples/thothii-installation.server.yaml'
'compose.yaml'
'deploy/compose.server.yaml'
'server.md'
)
fi
for expected in "${expected_steps[@]}"; 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"
}
verify_read_only_workspace_runtime_contract() {
python3 - "$root" <<'PY'
import pathlib, sys, yaml
root = pathlib.Path(sys.argv[1])
compose = yaml.safe_load((root / "compose.yaml").read_text())
services = compose["services"]
core = services["core"]
maintenance = services["workspace-maintenance"]
environment = core["environment"]
for forbidden in ("THT_WORKSPACE_GIT_AUTHOR_NAME", "THT_WORKSPACE_GIT_AUTHOR_EMAIL"):
if forbidden in environment:
raise SystemExit(f"compose retains Git write identity: {forbidden}")
for key, value in {
"THT_WORKSPACE_SECRET_STORE_ROOT": "/data/workspace-secrets",
"THT_WORKSPACE_SECRET_RUNTIME_ROOT": "/tmp/thothii-workspace-secrets",
}.items():
if environment.get(key) != value or maintenance["environment"].get(key) != value:
raise SystemExit(f"workspace secret setting missing from core/maintenance: {key}")
if "workspace-secrets" not in compose["volumes"]:
raise SystemExit("workspace-secrets persistent volume is missing")
if not any("workspace-secrets:/data/workspace-secrets" in str(value) for value in core["volumes"]):
raise SystemExit("core does not persist the workspace secret vault")
if not any(mount.get("source") == "workspace-secrets" and mount.get("target") == "/data/workspace-secrets"
for mount in maintenance["volumes"] if isinstance(mount, dict)):
raise SystemExit("workspace-maintenance cannot use the encrypted workspace vault")
dockerfile = (root / "docker/core.Dockerfile").read_text()
if "/data/workspace-secrets" not in dockerfile:
raise SystemExit("core image does not pre-create the workspace secret volume target")
checked = [
root / "docs/install/local-workspace-registry.md",
root / "docs/install/server-workspace-registry.md",
root / "docs/install/local.md",
root / "docs/install/server.md",
root / "docs/install/psd-workspace-setup.md",
root / "docs/guida-utente.md",
root / "deploy/workspace-registry.env.example",
root / "deploy/env/local.env.example",
root / "deploy/env/server.env.example",
root / "deploy/psd/operator.env.example",
root / "docs/install/examples/thothii-installation.local.yaml",
root / "docs/install/examples/thothii-installation.server.yaml",
]
joined = "\n".join(path.read_text() for path in checked)
for forbidden in (
"THT_WORKSPACE_GIT_AUTHOR_NAME",
"THT_WORKSPACE_GIT_AUTHOR_EMAIL",
"connector-secrets.local.yaml",
"connector-secrets.server.yaml",
"THT_WORKSPACE_BINDINGS_ENV_FILE",
"POST /workspaces/publish",
"POST /workspaces/import",
"Import workspace bundle",
):
if forbidden in joined:
raise SystemExit(f"active workspace documentation retains obsolete contract: {forbidden}")
for required in (
"GitHub, GitLab, or Gitea",
"read-only consumer",
"workspace-secrets",
"write-only",
"previous active revision",
):
if required.lower() not in joined.lower():
raise SystemExit(f"active workspace documentation lacks required concept: {required}")
ui = (root / "frontend/src/shell/WorkspaceManager.tsx").read_text()
for required in (
"Create a workspace repository",
"Update workspace repository",
"No workspace selection is required",
"Temporary files are deleted after the test",
):
if required not in ui:
raise SystemExit(f"Workspace management lacks required explanation: {required}")
for forbidden in ("Import bundle", "Export bundle", "localStorage"):
if forbidden in ui:
raise SystemExit(f"Workspace management retains obsolete behavior: {forbidden}")
PY
echo "read-only workspace repository and encrypted runtime-secret contract passed"
}
verify_local_installation_example() {
local example="$root/docs/install/examples/thothii-installation.local.yaml"
[[ -f "$example" ]] || {
echo "missing local installation example: docs/install/examples/thothii-installation.local.yaml" >&2
return 1
}
local fixture source_copy operator_dir copied_example env_file auth_config_root
fixture="$(mktemp -d "${tmp_prefix}thoth local install.XXXXXX")"
trap 'rm -rf "$fixture"' RETURN
[[ "$fixture" == *" "* ]] || {
echo "local installation fixture path does not contain spaces" >&2
return 1
}
source_copy="$fixture/ThothII source"
operator_dir="$fixture/operator files"
auth_config_root="$operator_dir/auth config"
mkdir -p "$source_copy/deploy/pi" "$operator_dir" "$auth_config_root"
cp "$root/compose.yaml" "$source_copy/compose.yaml"
cp "$root/deploy/compose.local.yaml" "$source_copy/deploy/compose.local.yaml"
cp "$root/deploy/compose.git-ssh.yaml" "$source_copy/deploy/compose.git-ssh.yaml"
cp "$root/deploy/pi/models.json" "$source_copy/deploy/pi/models.json"
cp "$root/deploy/pi/settings.json" "$source_copy/deploy/pi/settings.json"
write_private "$operator_dir/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-local-pi-key"}}'
write_private "$operator_dir/thothii.secrets" 'THT_MODEL_API_KEY=fixture-local-model-key'
write_private "$operator_dir/git-ssh-key" 'fixture-local-ssh-key'
write_private "$operator_dir/git-known-hosts" 'fixture-local-known-hosts'
env_file="$source_copy/deploy/env/local.env"
mkdir -p "$source_copy/deploy/env"
printf '%s\n' \
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
"PI_AUTH_FILE=$operator_dir/pi-auth.json" \
"THT_SECRETS_FILE=$operator_dir/thothii.secrets" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$operator_dir/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$operator_dir/git-known-hosts" \
"THT_AUTH_CONFIG_ROOT=$auth_config_root" \
>"$env_file"
copied_example="$fixture/thothii-installation.yaml"
local contents
contents="$(<"$example")"
contents="${contents//\/absolute\/path\/to\/ThothII/$source_copy}"
contents="${contents//\/absolute\/path\/to\/thothii-operator/$operator_dir}"
printf '%s\n' "$contents" >"$copied_example"
local profile project_directory descriptor_env value
local -a overrides files
profile="$(sed -n 's/^profile: \([^[:space:]]*\)$/\1/p' "$copied_example")"
project_directory="$(sed -n 's/^projectDirectory: "\(.*\)"$/\1/p' "$copied_example")"
descriptor_env="$(sed -n 's/^envFile: "\(.*\)"$/\1/p' "$copied_example")"
while IFS= read -r value; do overrides+=("$value"); done < <(sed -n 's/^ - "\(.*\)"$/\1/p' "$copied_example")
[[ "$profile" == local && "$project_directory" == "$source_copy" && "$descriptor_env" == "$env_file" ]] || {
echo "local installation example does not resolve its required fields" >&2
return 1
}
[[ "${#overrides[@]}" -eq 1 && "${overrides[0]}" == "$source_copy/deploy/compose.git-ssh.yaml" ]] || {
echo "local installation example does not select the expected optional overrides" >&2
return 1
}
files=(-f "$project_directory/compose.yaml" -f "$project_directory/deploy/compose.$profile.yaml")
for value in "${overrides[@]}"; do files+=(-f "$value"); done
local rendered="$fixture/local-installation.json"
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
"${files[@]}" config --format json >"$rendered"
node - "$rendered" "$auth_config_root" <<'NODE'
const fs = require("fs");
const [path, authConfigRoot] = process.argv.slice(2);
const config = JSON.parse(fs.readFileSync(path, "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,embedding,embedding-model-init,frontend,qdrant") {
throw new Error("local installation example must render the internal semantic stack");
}
const authMount = (config.services.core.volumes || []).find(
(mount) => mount.target === "/run/thothii-auth",
);
if (!authMount || authMount.source !== authConfigRoot || authMount.read_only !== true) {
throw new Error("local installation example must mount its fixture auth root read-only");
}
const output = JSON.stringify(config);
for (const secret of [
"fixture-local-pi-key",
"fixture-local-model-key",
"fixture-local-ssh-key",
"fixture-local-known-hosts",
"fixture-local-dwh-password",
]) {
if (output.includes(secret)) throw new Error("local installation rendering exposed a fixture secret");
}
NODE
echo "local installation example rendered from path with spaces passed"
}
verify_server_installation_example() {
local example="$root/docs/install/examples/thothii-installation.server.yaml"
[[ -f "$example" ]] || {
echo "missing server installation example: docs/install/examples/thothii-installation.server.yaml" >&2
return 1
}
local fixture source_copy operator_dir copied_example env_file backup_root auth_config_root
fixture="$(mktemp -d "${tmp_prefix}thoth server install.XXXXXX")"
trap 'rm -rf "$fixture"' RETURN
[[ "$fixture" == *" "* ]] || {
echo "server installation fixture path does not contain spaces" >&2
return 1
}
source_copy="$fixture/ThothII server source"
operator_dir="$fixture/server operator files"
backup_root="$fixture/server backups"
auth_config_root="$operator_dir/auth config"
mkdir -p "$source_copy/deploy/pi" "$source_copy/deploy/workspaces" \
"$operator_dir/data/workspace-secrets" "$operator_dir/pi-state" "$operator_dir/workspace-registry" \
"$auth_config_root" "$backup_root"
"$root/scripts/prepare-server-pi-state.sh" \
"$operator_dir/pi-state" "$(id -u)" "$(id -g)" >/dev/null
cp "$root/compose.yaml" "$source_copy/compose.yaml"
cp "$root/deploy/compose.server.yaml" "$source_copy/deploy/compose.server.yaml"
cp "$root/deploy/compose.session-server.yaml.example" \
"$source_copy/deploy/compose.session-server.yaml.example"
cp "$root/deploy/compose.git-ssh.yaml" "$source_copy/deploy/compose.git-ssh.yaml"
cp "$root/deploy/pi/models.json" "$source_copy/deploy/pi/models.json"
cp "$root/deploy/pi/settings.json" "$source_copy/deploy/pi/settings.json"
cp "$root/deploy/workspaces/server-sessions.yaml.example" \
"$source_copy/deploy/workspaces/server-sessions.yaml.example"
write_private "$operator_dir/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-server-pi-key"}}'
write_private "$operator_dir/thothii.secrets" 'THT_MODEL_API_KEY=fixture-server-model-key'
write_private "$operator_dir/git-ssh-key" 'fixture-server-ssh-key'
write_private "$operator_dir/git-known-hosts" 'fixture-server-known-hosts'
write_private "$operator_dir/session-runtime-password" 'fixture-server-session-runtime-password'
write_private "$operator_dir/session-migrator-password" 'fixture-server-session-migrator-password'
write_private "$operator_dir/session-ca.pem" 'fixture-server-session-ca'
env_file="$operator_dir/server.env"
printf '%s\n' \
'THOTH_SERVER_BIND=127.0.0.1' \
'THOTH_HTTP_PORT=8080' \
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
'THT_WORKSPACE_GIT_BRANCH=main' \
"PI_AUTH_FILE=$operator_dir/pi-auth.json" \
"THT_SECRETS_FILE=$operator_dir/thothii.secrets" \
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$operator_dir/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$operator_dir/git-known-hosts" \
"THT_DATA_ROOT=$operator_dir/data" \
"THT_PI_STATE_ROOT=$operator_dir/pi-state" \
"THT_WORKSPACE_REGISTRY_ROOT=$operator_dir/workspace-registry" \
"THT_BACKUP_ROOT=$backup_root" \
"THT_AUTH_CONFIG_ROOT=$auth_config_root" \
"THT_SERVER_WORKSPACE_CONFIG=$source_copy/deploy/workspaces/server-sessions.yaml.example" \
'THT_LLM_URL=https://llm.example.invalid' \
'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=$operator_dir/session-runtime-password" \
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$operator_dir/session-migrator-password" \
"THT_SESSION_CA_SOURCE=$operator_dir/session-ca.pem" \
>"$env_file"
copied_example="$fixture/thothii-installation.yaml"
local contents
contents="$(<"$example")"
contents="${contents//\/absolute\/path\/to\/ThothII/$source_copy}"
contents="${contents//\/absolute\/path\/to\/thothii-server-operator/$operator_dir}"
printf '%s\n' "$contents" >"$copied_example"
local profile project_directory descriptor_env value
local -a overrides files
profile="$(sed -n 's/^profile: \([^[:space:]]*\)$/\1/p' "$copied_example")"
project_directory="$(sed -n 's/^projectDirectory: "\(.*\)"$/\1/p' "$copied_example")"
descriptor_env="$(sed -n 's/^envFile: "\(.*\)"$/\1/p' "$copied_example")"
while IFS= read -r value; do overrides+=("$value"); done < <(sed -n 's/^ - "\(.*\)"$/\1/p' "$copied_example")
[[ "$profile" == server && "$project_directory" == "$source_copy" && "$descriptor_env" == "$env_file" ]] || {
echo "server installation example does not resolve its required fields" >&2
return 1
}
[[ "${#overrides[@]}" -eq 2 && "${overrides[0]}" == "$source_copy/deploy/compose.session-server.yaml.example" \
&& "${overrides[1]}" == "$source_copy/deploy/compose.git-ssh.yaml" ]] || {
echo "server installation example does not select the expected optional overrides" >&2
return 1
}
files=(-f "$project_directory/compose.yaml" -f "$project_directory/deploy/compose.$profile.yaml")
for value in "${overrides[@]}"; do files+=(-f "$value"); done
local rendered="$fixture/server-installation.json"
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
"${files[@]}" config --format json >"$rendered"
node - "$rendered" "$auth_config_root" <<'NODE'
const fs = require("fs");
const [path, authConfigRoot] = process.argv.slice(2);
const config = JSON.parse(fs.readFileSync(path, "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,embedding,embedding-model-init,frontend,qdrant") {
throw new Error("server installation example must render the internal semantic stack");
}
const core = config.services.core;
const frontend = config.services.frontend;
const authMount = (core.volumes || []).find((mount) => mount.target === "/run/thothii-auth");
if (!authMount || authMount.source !== authConfigRoot || authMount.read_only !== true) {
throw new Error("server installation example must mount its fixture auth root read-only");
}
if (core.environment?.THOTH_PUBLIC_EXPOSURE !== "true" ||
core.environment?.THT_AUTH_CONFIG_FILE !== "/run/thothii-auth/auth.yaml") {
throw new Error("server installation example must expose only the authenticated frontend");
}
if ((core.ports || []).length !== 0) throw new Error("server installation example published core");
const ports = frontend.ports || [];
if (ports.length !== 1 || ports[0].host_ip !== "127.0.0.1" || Number(ports[0].target) !== 8080) {
throw new Error("server installation example must publish only loopback frontend");
}
const rendered = JSON.stringify(config);
if (/omics_portal|chirone|localllm_default|datamart-builder/i.test(rendered)) {
throw new Error("server installation example contains application coupling");
}
for (const secret of [
"fixture-server-pi-key", "fixture-server-model-key", "fixture-server-ssh-key",
"fixture-server-known-hosts", "fixture-server-dwh-password",
"fixture-server-session-runtime-password", "fixture-server-session-migrator-password",
"fixture-server-session-ca",
]) {
if (rendered.includes(secret)) throw new Error("server installation rendering exposed a fixture secret");
}
NODE
echo "server installation example rendered from path with spaces passed"
local migration_rendered="$fixture/server-migration.json"
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
"${files[@]}" --profile session-migrate config --format json >"$migration_rendered"
node - "$migration_rendered" <<'NODE'
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const services = config.services || {};
if (!services.core || !services["session-migrate"]) throw new Error("server migration profile is missing core or session-migrate");
if (services.core.image !== services["session-migrate"].image) throw new Error("source migration image differs from core");
if (services["session-migrate"].build) throw new Error("source migration service unexpectedly declares a build");
NODE
local pinned_template="$fixture/pinned-template.yaml" pinned_override="$operator_dir/pinned-images.yaml"
awk '
/^## Build locally or select pinned images$/ { section=1; next }
section && /^```yaml$/ { code=1; next }
code && /^```$/ { exit }
code { print }
' "$root/docs/install/server.md" >"$pinned_template"
sed \
-e "s#registry.example.com/thothii/core@sha256:<64-lowercase-hex-digits>#registry.example.com/thothii/core@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#g" \
-e "s#registry.example.com/thothii/frontend@sha256:<64-lowercase-hex-digits>#registry.example.com/thothii/frontend@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb#g" \
"$pinned_template" >"$pinned_override"
chmod 0600 "$pinned_override"
local pinned_rendered="$fixture/server-pinned-migration.json"
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
"${files[@]}" -f "$pinned_override" --profile session-migrate config --format json >"$pinned_rendered"
node - "$pinned_rendered" <<'NODE'
const fs = require("fs");
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const core = config.services?.core;
const frontend = config.services?.frontend;
const migrator = config.services?.["session-migrate"];
if (!core || !frontend || !migrator) throw new Error("pinned migration profile lacks core, frontend, or session-migrate");
if (core.image !== migrator.image || !/@sha256:[a-f0-9]{64}$/.test(core.image)) {
throw new Error("pinned migration image does not equal the exact core digest");
}
if (!/@sha256:[a-f0-9]{64}$/.test(frontend.image)) throw new Error("frontend is not pinned by exact digest");
for (const [name, service] of Object.entries({core, frontend, migrator})) {
if (service.build) throw new Error(name + " retained a local build in pinned mode");
if (/:local$/.test(service.image || "")) throw new Error(name + " retained a local image in pinned mode");
}
NODE
echo "server pinned migration image fixture passed"
local checksum_root="$fixture/root-only-checksum"
mkdir -m 0700 "$checksum_root"
printf 'fixture backup bytes\n' >"$checksum_root/runtime-data.tgz"
/bin/sh -ceu 'cd "$1"; sha256sum runtime-data.tgz > SHA256SUMS; sha256sum --check SHA256SUMS' sh "$checksum_root" >/dev/null
printf 'corruption\n' >>"$checksum_root/runtime-data.tgz"
if (cd "$checksum_root" && sha256sum --check SHA256SUMS) >/dev/null 2>&1; then
echo "corrupted server backup checksum fixture was accepted" >&2
return 1
fi
echo "server backup checksum root-only fixture passed"
}
write_private() {
local path="$1" value="$2"
printf '%s\n' "$value" >"$path"
chmod 0600 "$path"
}
verify_compose_fixtures() {
local fixture profile rendered auth_config_root
fixture="$(mktemp -d "${tmp_prefix}thoth-install-fixtures.XXXXXX")"
trap 'rm -rf "$fixture"' RETURN
auth_config_root="$fixture/auth-config"
mkdir -p "$fixture/data/workspace-secrets" "$fixture/pi-state" \
"$fixture/workspace-registry" "$auth_config_root"
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/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_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_GIT_SSH_KEY_FILE=$fixture/git-ssh-key" \
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture/git-known-hosts" \
"THT_DATA_ROOT=$fixture/data" \
"THT_PI_STATE_ROOT=$fixture/pi-state" \
"THT_WORKSPACE_REGISTRY_ROOT=$fixture/workspace-registry" \
"THT_AUTH_CONFIG_ROOT=$auth_config_root" \
"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"
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"
)
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture/operator.env" \
"${files[@]}" config --format json >"$rendered"
node - "$rendered" "$profile" "$auth_config_root" <<'NODE'
const fs = require("fs");
const [path, profile, authConfigRoot] = process.argv.slice(2);
const config = JSON.parse(fs.readFileSync(path, "utf8"));
if (Object.keys(config.services).sort().join(",") !== "core,embedding,embedding-model-init,frontend,qdrant") {
throw new Error(profile + ": mandatory stack must include the internal semantic services");
}
const core = config.services.core;
const authMount = (core.volumes || []).find((mount) => mount.target === "/run/thothii-auth");
if (!authMount || authMount.source !== authConfigRoot || authMount.read_only !== true) {
throw new Error(profile + ": core must mount its fixture auth root read-only");
}
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);
}
}
const secretTargets = new Set((core.secrets || []).map((secret) => secret.target));
for (const target of [
"thothii.secrets",
]) {
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-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_internal_semantic_infrastructure_docs
echo "internal semantic infrastructure documentation contract passed"
verify_read_only_workspace_runtime_contract
verify_workspace_evidence_contract
verify_local_guide
verify_windows_line_endings_guide
verify_pi_management_guide
verify_server_guide
verify_reverse_proxy_nginx_guide
verify_reverse_proxy_caddy_guide
verify_local_installation_example
verify_server_installation_example
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; }
if [[ "$profile" == local ]]; then
verify_internal_semantic_infrastructure_docs
verify_read_only_workspace_runtime_contract
verify_workspace_evidence_contract
verify_local_guide
verify_windows_line_endings_guide
verify_pi_management_guide
verify_local_installation_example
else
verify_internal_semantic_infrastructure_docs
verify_read_only_workspace_runtime_contract
verify_workspace_evidence_contract
verify_server_guide
verify_reverse_proxy_nginx_guide
verify_reverse_proxy_caddy_guide
verify_caddy_effective_proxy_guide
"$root/scripts/test-server-operator-permissions.sh"
verify_server_installation_example
fi
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 "usage: $0 --fixtures-only | --profile {local|server}" >&2
exit 2
;;
esac