docs: define workspace evidence registry contract

This commit is contained in:
2026-08-09 20:37:22 +02:00
parent a580c4ca8a
commit 80aa989523
8 changed files with 704 additions and 6 deletions
@@ -12,6 +12,7 @@ trap 'rm -f "$output" "$verifier_functions"; rm -rf "$negative_root"' EXIT HUP I
for fixture in \
"internal semantic infrastructure documentation contract" \
"workspace Evidence documentation contract" \
"local installation guide contract" \
"source update fail-closed semantics" \
"Windows line-ending recovery guide contract" \
@@ -430,6 +431,126 @@ if [[ $adapted_status -eq 0 ]] || ! grep -Fq "Caddy adapted frontend path bypass
fi
negative_failures=0
expect_evidence_fixture_rejected() {
local label="$1" target="$2" mutation="$3" expected_error="$4"
local fixture_root="$negative_root/evidence-${label// /-}"
local fixture_output="$fixture_root/output"
# Before Task 7's dedicated verifier exists, every mutation is deliberately accepted. This
# makes the complete Evidence test matrix RED without allowing command-not-found to abort it.
if ! declare -F verify_workspace_evidence_contract >/dev/null; then
echo "negative fixture accepted: $label (Evidence verifier missing)" >&2
negative_failures=$((negative_failures + 1))
return
fi
mkdir -p \
"$fixture_root/deploy/workspaces" \
"$fixture_root/docs/contracts" \
"$fixture_root/docs/install/examples"
cp "$root/deploy/workspaces/example.yaml" "$fixture_root/deploy/workspaces/example.yaml"
cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example"
cp "$root/docs/contracts/workspace-evidence-v3.md" \
"$fixture_root/docs/contracts/workspace-evidence-v3.md"
cp "$root/docs/install/local-workspace-registry.md" \
"$fixture_root/docs/install/local-workspace-registry.md"
cp "$root/docs/install/server-workspace-registry.md" \
"$fixture_root/docs/install/server-workspace-registry.md"
cp "$root/docs/install/examples/workspace-bindings.env.example" \
"$fixture_root/docs/install/examples/workspace-bindings.env.example"
python3 - "$fixture_root/$target" "$mutation" <<'PY'
import pathlib, sys, yaml
path = pathlib.Path(sys.argv[1])
mutation = sys.argv[2]
original = path.read_text()
changed = original
if mutation == "layout-omitted":
changed = original.replace("│ ├── example/evidence/...\n", "", 1)
elif mutation == "same-commit-omitted":
changed = original.replace(
"| Revision identity | The descriptor blob and filesystem Evidence root tree are checked at the same 40-hex Git commit. |\n",
"",
1,
)
elif mutation in {"absolute-filesystem", "cross-workspace"}:
document = yaml.safe_load(original)
document["evidence"]["source"]["uri"] = (
"/srv/evidence" if mutation == "absolute-filesystem"
else "workspace-content/example/evidence"
)
changed = yaml.safe_dump(document, sort_keys=False)
elif mutation == "wrong-docs-directory":
changed = original.replace(
"workspace-docs/\n ├── example/{contract.env.example,README.md}\n └── another/{contract.env.example,README.md}",
"workspaces/<id>.env.example\nworkspaces/<id>.md",
1,
)
elif mutation == "http-file-boundary-omitted":
changed = original.replace(
"| Signed HTTP | `THT_WS_<NAMESPACE>_EVIDENCE_SIGNED_URLS_FILE` | Required for `signed_urls_file`; nonempty JSON string array in declared-URI order; query-stripped identities must match `uris`. |\n",
"",
1,
)
elif mutation == "s3-pair-boundary-omitted":
changed = original.replace(
"| Static S3 pair | `THT_WS_<NAMESPACE>_EVIDENCE_ACCESS_KEY_FILE` and `THT_WS_<NAMESPACE>_EVIDENCE_SECRET_KEY_FILE` | Required together for `static_files`. |\n",
"",
1,
)
elif mutation == "s3-token-boundary-omitted":
changed = original.replace(
"| Static S3 session | `THT_WS_<NAMESPACE>_EVIDENCE_SESSION_TOKEN_FILE` | Optional, and valid only with the required access/secret pair. |\n",
"",
1,
)
elif mutation == "credential-literal":
changed = original + "\nTHT_WS_STATIC_S3_EVIDENCE_SECRET_KEY=AKIAEXAMPLECREDENTIAL\n"
elif mutation == "signed-query-example":
signed_query = "https://evidence.example.invalid/report" + "?X-Amz-Signature=unsafe"
changed = original + f"\nTHT_EVIDENCE_URI={signed_query}\n"
elif mutation == "unsafe-placeholder":
changed = original.replace(
"/run/secrets/signed-http-evidence-urls.json", "changeme", 1
)
elif mutation == "p1-scope-inversion":
changed = original.replace(
"P1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC.",
"P1 materializes, extracts, and indexes Evidence before publication.",
1,
)
elif mutation == "config-ordering":
changed = original.replace(
"tht config check -c <path>", "tht -c <path> config check", 1
)
elif mutation == "acceptance-conflation":
changed = original.replace("manual acceptance: PENDING\n", "", 1)
elif mutation == "curator-order":
second = "2. Add source bytes below `workspace-content/<id>/evidence`, then commit and push."
third = "3. Validate and publish the descriptor against that base commit."
changed = original.replace(second + "\n" + third, third + "\n" + second, 1)
else:
raise SystemExit(f"unknown Evidence mutation: {mutation}")
if changed == original:
raise SystemExit(f"Evidence mutation made no change: {mutation}")
path.write_text(changed)
PY
set +e
verify_workspace_evidence_contract "$fixture_root" >"$fixture_output" 2>&1
local status=$?
set -e
if [[ $status -eq 0 ]]; then
echo "negative fixture accepted: $label" >&2
cat "$fixture_output" >&2
negative_failures=$((negative_failures + 1))
elif ! grep -Fq -- "$expected_error" "$fixture_output"; then
echo "negative fixture failed for the wrong reason: $label" >&2
cat "$fixture_output" >&2
negative_failures=$((negative_failures + 1))
fi
}
expect_guide_rejected() {
local label="$1" validator="$2" source_guide="$3" relative_path="$4"
local mutation="$5" expected_error="$6"
@@ -784,6 +905,52 @@ expect_guide_rejected \
"$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md powershell-crlf-failure \
"PowerShell CRLF repair lacks failure propagation: Assert-NativeSuccess 'index export'"
expect_evidence_fixture_rejected \
"canonical Evidence layout omitted" docs/contracts/workspace-evidence-v3.md layout-omitted \
"missing canonical Evidence layout"
expect_evidence_fixture_rejected \
"same revision ownership omitted" docs/contracts/workspace-evidence-v3.md same-commit-omitted \
"missing same-revision ownership"
expect_evidence_fixture_rejected \
"absolute filesystem Evidence path" deploy/workspaces/example.yaml absolute-filesystem \
"noncanonical filesystem Evidence URI"
expect_evidence_fixture_rejected \
"cross-workspace Evidence path" deploy/workspaces/psd.yaml.example cross-workspace \
"Evidence namespace mismatch"
expect_evidence_fixture_rejected \
"generated docs in wrong directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory \
"generated docs path invalid"
expect_evidence_fixture_rejected \
"signed HTTP file boundary omitted" docs/contracts/workspace-evidence-v3.md http-file-boundary-omitted \
"missing signed HTTP file boundary"
expect_evidence_fixture_rejected \
"static S3 pair boundary omitted" docs/contracts/workspace-evidence-v3.md s3-pair-boundary-omitted \
"missing static S3 file boundary"
expect_evidence_fixture_rejected \
"static S3 optional token boundary omitted" docs/contracts/workspace-evidence-v3.md s3-token-boundary-omitted \
"missing static S3 session-token boundary"
expect_evidence_fixture_rejected \
"credential literal in public bindings" docs/install/examples/workspace-bindings.env.example credential-literal \
"credential literal forbidden"
expect_evidence_fixture_rejected \
"signed query in public bindings" docs/install/examples/workspace-bindings.env.example signed-query-example \
"query-bearing public URI forbidden"
expect_evidence_fixture_rejected \
"unsafe Evidence file placeholder" docs/install/examples/workspace-bindings.env.example unsafe-placeholder \
"unsafe file placeholder/path"
expect_evidence_fixture_rejected \
"P1 Evidence scope inversion" docs/contracts/workspace-evidence-v3.md p1-scope-inversion \
"P1 scope violation"
expect_evidence_fixture_rejected \
"config check option reordered" docs/contracts/workspace-evidence-v3.md config-ordering \
"exact config-check ordering missing"
expect_evidence_fixture_rejected \
"acceptance states conflated" docs/contracts/workspace-evidence-v3.md acceptance-conflation \
"separate automated/manual states missing"
expect_evidence_fixture_rejected \
"local curator flow reordered" docs/install/local-workspace-registry.md curator-order \
"curator flow out of order"
if (( negative_failures != 0 )); then
echo "$negative_failures unsafe installation-document fixtures were accepted" >&2
exit 1
+273
View File
@@ -235,6 +235,276 @@ if embedding["dimensions"] != 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"
bindings_path = base / "docs/install/examples/workspace-bindings.env.example"
paths = [contract_path, local_path, server_path, bindings_path]
for path in paths:
if not path.is_file():
raise SystemExit(f"missing workspace Evidence contract input: {path.relative_to(base)}")
# Generic descriptors must publish an explicit, ID-derived canonical filesystem contract.
for relative in ("deploy/workspaces/example.yaml", "deploy/workspaces/psd.yaml.example"):
path = base / relative
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-content/{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()
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": "workspace-content/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, exports, and rendered YAML never expose file contents.",
"THT_WORKSPACE_SECRET_ROOTS",
"readable regular file",
"strictly below",
"Content-only revision",
"read-only Evidence summary",
"excludes Evidence bytes",
]
for phrase in required_contract_phrases:
if phrase not in contract:
raise SystemExit(f"workspace Evidence contract lacks required rule: {phrase}")
# The canonical one-repository tree is exact, including generated docs outside workspaces/.
legacy_docs = re.compile(r"workspaces/(?:<[^>]+>|[^\s`/]+)\.(?:env\.example|md)")
all_public = "\n".join(path.read_text() for path in paths)
if legacy_docs.search(all_public) or "workspaces/<id>.env.example" in all_public:
raise SystemExit("generated docs path invalid")
required_tree_lines = [
"registry.git/", "├── workspaces/", "│ ├── example.yaml", "│ └── another.yaml",
"├── workspace-content/", "│ ├── example/evidence/...",
"│ └── another/evidence/...", "└── workspace-docs/",
" ├── example/{contract.env.example,README.md}",
" └── another/{contract.env.example,README.md}",
]
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 "same 40-hex Git commit" not in revision_text or "descriptor blob" not in revision_text or "root tree" not in revision_text:
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")
p1 = relationships.get("P1", "")
p6 = relationships.get("P6", "")
if not all(token in p1 for token in ("lexical URI", "Git tree", "same commit", "does not recursively inspect nested symlinks")):
raise SystemExit("missing P1 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 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC."
if no_scope not in contract or re.search(r"P1\s+(?:materializes|extracts|indexes)", contract, re.IGNORECASE):
raise SystemExit("P1 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 ("nonempty JSON string array", "declared-URI order", "query-stripped identities")
):
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",
)):
raise SystemExit("missing static S3 file boundary")
s3_token = " ".join(installation_rows.get("Static S3 session", []))
if "THT_WS_<NAMESPACE>_EVIDENCE_SESSION_TOKEN_FILE" not in s3_token or "Optional" not in s3_token:
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 = [
"Clone the one shared registry", "workspace-content/<id>/evidence", "commit and push",
"Validate and publish the descriptor against that base commit",
"workspace-docs/<id>/contract.env.example", "workspace-docs/<id>/README.md",
"Evidence `*_FILE` files outside Git", "THT_WORKSPACE_SECRET_ROOTS", "`*_SOURCE` paths",
"tht config check -c <path>", "P2/P6 later performs preprocessing and materialization",
]
for guide in (local_path, server_path):
text = guide.read_text()
match = re.search(
r"^## Curator flow for shared-registry Evidence\s*$\n(.*?)(?=^## |\Z)",
text,
re.MULTILINE | re.DOTALL,
)
if not match:
raise SystemExit(f"{guide.name}: missing curator flow")
section = match.group(1)
positions = [section.find(token) for token in flow_tokens]
if any(position < 0 for position in positions) or positions != sorted(positions):
raise SystemExit(f"{guide.name}: curator flow out of order")
# Parse dotenv assignments in the core example and fenced public guide blocks. Public examples may
# contain paths and query-free identities, but never credential values or unsafe placeholders.
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 "${TMPDIR:-/tmp}/thoth-vector-backup-help.XXXXXX")"
@@ -1756,6 +2026,7 @@ case "$mode" in
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
verify_internal_semantic_infrastructure_docs
echo "internal semantic infrastructure documentation contract passed"
verify_workspace_evidence_contract
verify_local_guide
verify_windows_line_endings_guide
verify_pi_management_guide
@@ -1774,12 +2045,14 @@ case "$mode" in
|| { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
if [[ "$profile" == local ]]; then
verify_internal_semantic_infrastructure_docs
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_workspace_evidence_contract
verify_server_guide
verify_reverse_proxy_nginx_guide
verify_reverse_proxy_caddy_guide