test: add schema v3 only absence gate
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
workspace:
|
||||
schema_version: 3
|
||||
id: task13-smoke
|
||||
name: Task 13 Smoke
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: analytics
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: task13-smoke
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
default: local-qwen/task13-smoke
|
||||
allowed: [local-qwen/task13-smoke]
|
||||
@@ -41,31 +41,7 @@ TASK13_CURRENT_IMAGE_OVERRIDE="$fixture/current-image.yaml"
|
||||
mkdir -p "$TASK13_REMOTE"
|
||||
|
||||
workspace="$fixture/task13-smoke.yaml"
|
||||
cat >"$workspace" <<'EOF'
|
||||
workspace:
|
||||
schema_version: 3
|
||||
id: task13-smoke
|
||||
name: Task 13 Smoke
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: analytics
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: task13-smoke
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
default: local-qwen/task13-smoke
|
||||
allowed: [local-qwen/task13-smoke]
|
||||
EOF
|
||||
cp "$root/scripts/fixtures/workspace-registry-task13.yaml" "$workspace"
|
||||
|
||||
if [[ "$profile" == local ]]; then
|
||||
task13_write_fixture_files
|
||||
|
||||
Executable
+716
@@ -0,0 +1,716 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression tests for the fail-closed schema-v3-only absence gate.
|
||||
set -euo pipefail
|
||||
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
gate="$project_root/scripts/verify-schema-v3-only.sh"
|
||||
gate_bash="${BASH:-bash}"
|
||||
sandbox="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v3-gate-test.XXXXXX")"
|
||||
fixture="$sandbox/fixture repository"
|
||||
output="$sandbox/output"
|
||||
real_git="$(command -v git)"
|
||||
canonical_schema="$project_root/backend/dist/workspaces/schema.js"
|
||||
canonical_server="$project_root/backend/dist/server.js"
|
||||
canonical_schema_checksum="$(cksum <"$canonical_schema")"
|
||||
canonical_server_checksum="$(cksum <"$canonical_server")"
|
||||
cleanup() {
|
||||
rm -rf "$sandbox"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
[[ ! -f "$output" ]] || cat "$output" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_fixture_descriptor() {
|
||||
local path="$1" workspace_key="${2:-workspace:}" schema_key="${3:- schema_version: 3}"
|
||||
printf '%s\n' "$workspace_key" "$schema_key" >"$path"
|
||||
cat >>"$path" <<'YAML'
|
||||
id: fixture-workspace
|
||||
name: Fixture Workspace
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: public
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: fixture-workspace
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [fixture/model]
|
||||
YAML
|
||||
}
|
||||
|
||||
seed_fixture() {
|
||||
rm -rf "$fixture"
|
||||
mkdir -p \
|
||||
"$fixture/backend/src/nested dir" \
|
||||
"$fixture/backend/scripts" \
|
||||
"$fixture/frontend/src/api" \
|
||||
"$fixture/deploy/workspaces" \
|
||||
"$fixture/scripts/fixtures"
|
||||
"$real_git" -C "$fixture" init -q
|
||||
"$real_git" -C "$fixture" config user.email fixture@example.invalid
|
||||
"$real_git" -C "$fixture" config user.name Fixture
|
||||
printf '%s\n' 'export const schemaVersion = 3;' >"$fixture/backend/src/server.ts"
|
||||
printf '%s\n' 'export const spaced = true;' >"$fixture/backend/src/nested dir/file name.ts"
|
||||
newline_path="$fixture/backend/src/line
|
||||
break.ts"
|
||||
printf '%s\n' 'export const newline = true;' >"$newline_path"
|
||||
printf '%s\n' 'export const currentWorkspace = true;' >"$fixture/frontend/src/api/workspaces.ts"
|
||||
printf '%s\n' 'export const productionCheck = true;' >"$fixture/backend/scripts/runtime-check.mjs"
|
||||
write_fixture_descriptor "$fixture/deploy/workspaces/example.yaml"
|
||||
write_fixture_descriptor "$fixture/deploy/workspaces/psd.yaml.example"
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'echo operator-smoke' >"$fixture/scripts/workspace-registry-smoke.sh"
|
||||
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
|
||||
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-windows.yaml"
|
||||
printf '%s\n' 'Write-Output "schema v3"' >"$fixture/scripts/test-windows-clone-contract.ps1"
|
||||
"$real_git" -C "$fixture" add .
|
||||
"$real_git" -C "$fixture" commit -qm seed
|
||||
}
|
||||
|
||||
commit_fixture() {
|
||||
"$real_git" -C "$fixture" add .
|
||||
"$real_git" -C "$fixture" commit -qm "$1"
|
||||
}
|
||||
|
||||
run_gate() {
|
||||
set +e
|
||||
"$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
|
||||
gate_status=$?
|
||||
set -e
|
||||
}
|
||||
|
||||
expect_pass() {
|
||||
local label="$1"
|
||||
run_gate
|
||||
[[ $gate_status -eq 0 ]] || fail "$label: expected pass, got status $gate_status"
|
||||
}
|
||||
|
||||
expect_rejected() {
|
||||
local label="$1" expected="$2"
|
||||
run_gate
|
||||
[[ $gate_status -eq 1 ]] || fail "$label: expected rejection status 1, got $gate_status"
|
||||
grep -Fq -- "$expected" "$output" || fail "$label: rejection did not identify $expected"
|
||||
}
|
||||
|
||||
# Clean tracked live paths, including spaces and an embedded newline, are NUL-safe.
|
||||
seed_fixture
|
||||
expect_pass "clean runtime fixture"
|
||||
|
||||
seed_fixture
|
||||
mkfifo "$fixture/scripts/runtime-fifo"
|
||||
expect_rejected "runtime FIFO" "scripts/runtime-fifo"
|
||||
|
||||
set +e
|
||||
"$gate_bash" "$gate" --help >"$output" 2>&1
|
||||
help_status=$?
|
||||
set -e
|
||||
[[ $help_status -eq 0 ]] || fail "gate help failed with status $help_status"
|
||||
grep -Fq 'Node' "$output" || fail "gate help omits the runtime-only Node dependency"
|
||||
grep -Fq 'backend/dist/workspaces/schema.js' "$output" \
|
||||
|| fail "gate help omits the runtime-only compiled schema dependency"
|
||||
grep -Fq 'scripts/verify-schema-v3-only-release.sh' "$output" \
|
||||
|| fail "gate help omits the durable release entry point"
|
||||
grep -Fq 'npm ci' "$output" || fail "gate help omits lockfile install order"
|
||||
|
||||
|
||||
# Prescribed symbols and migration markers are case-insensitive substrings.
|
||||
seed_fixture
|
||||
printf '%s\n' 'export type WorkspaceV2Compat = unknown;' >"$fixture/backend/src/legacy.ts"
|
||||
commit_fixture backend-symbol
|
||||
expect_rejected "backend prescribed derivative symbol" "backend/src/legacy.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export type LegacyWorkspace = unknown;' >"$fixture/backend/src/legacy-workspace.ts"
|
||||
commit_fixture legacy-workspace-symbol
|
||||
expect_rejected "exact LegacyWorkspace symbol" "backend/src/legacy-workspace.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const status = "MIGRATION_REQUIRED";' >"$fixture/backend/src/status.ts"
|
||||
commit_fixture backend-case-insensitive-marker
|
||||
expect_rejected "case-insensitive marker" "backend/src/status.ts"
|
||||
|
||||
# Every forbidden category is applied to every recursive policy root without extension filters.
|
||||
policy_roots=(backend/src frontend/src backend/scripts scripts)
|
||||
policy_extensions=(ts py js yaml.example)
|
||||
policy_names=(WorkspaceV2 LegacyWorkspace migration_required 'revision.state')
|
||||
for category_index in 0 1 2 3; do
|
||||
for root_index in 0 1 2 3; do
|
||||
seed_fixture
|
||||
matrix_root="${policy_roots[$root_index]}"
|
||||
matrix_extension="${policy_extensions[$root_index]}"
|
||||
matrix_path="$matrix_root/matrix-$category_index.$matrix_extension"
|
||||
mkdir -p "${fixture:?}/$matrix_root"
|
||||
printf '%s\n' "${policy_names[$category_index]}" >"$fixture/$matrix_path"
|
||||
commit_fixture "policy-matrix-$category_index-$root_index"
|
||||
expect_rejected "policy category $category_index root $matrix_root" "$matrix_path"
|
||||
done
|
||||
done
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const status = "migration_required";' >"$fixture/frontend/src/api/workspaces.ts"
|
||||
commit_fixture frontend-marker
|
||||
expect_rejected "frontend migration status" "frontend/src/api/workspaces.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const blocked = record.revision.state !== "operational";' >"$fixture/frontend/src/api/workspaces.ts"
|
||||
commit_fixture frontend-revision-state
|
||||
expect_rejected "frontend revision state gate" "frontend/src/api/workspaces.ts"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/scripts/nested/production"
|
||||
printf '%s\n' 'const helper = "migrate-v2-qdrant.js";' >"$fixture/backend/scripts/nested/production/runtime.mjs"
|
||||
commit_fixture nested-mjs
|
||||
expect_rejected "nested backend production script" "backend/scripts/nested/production/runtime.mjs"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'const historical = entry.revision.state;' >"$fixture/backend/scripts/runtime-check.mjs"
|
||||
commit_fixture backend-historical-state
|
||||
expect_rejected "backend historical revision state" "backend/scripts/runtime-check.mjs"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'node backend/dist/workspaces/MIGRATE-LEGACY.js' >"$fixture/scripts/workspace-registry-smoke.sh"
|
||||
commit_fixture operator-migrator
|
||||
expect_rejected "operator smoke migrator" "scripts/workspace-registry-smoke.sh"
|
||||
|
||||
# Workspace YAML embedded in live non-test deployment scripts is validated structurally.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/heredoc-smoke.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cat <<'YAML'
|
||||
workspace:
|
||||
schema_version: 2
|
||||
YAML
|
||||
EOF
|
||||
commit_fixture script-heredoc-v2
|
||||
expect_rejected "deployment-script workspace schema" "scripts/operators/heredoc-smoke.sh"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/quoted-heredoc-smoke.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cat <<'YAML'
|
||||
"workspace" :
|
||||
'schema_version' : 0x2
|
||||
YAML
|
||||
EOF
|
||||
commit_fixture script-quoted-heredoc
|
||||
expect_rejected "quoted deployment-script workspace schema" "scripts/operators/quoted-heredoc-smoke.sh"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
{
|
||||
printf '%s\n' '#!/usr/bin/env bash' "cat <<'---'"
|
||||
printf '%s \n' '---'
|
||||
printf '%s\n' 'workspace:' ' schema_version: 2' '---'
|
||||
} >"$fixture/scripts/operators/exact-close-smoke.sh"
|
||||
"$gate_bash" -n "$fixture/scripts/operators/exact-close-smoke.sh"
|
||||
commit_fixture script-exact-heredoc-close
|
||||
expect_rejected "heredoc false close with trailing blanks" "scripts/operators/exact-close-smoke.sh"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/double-quoted-backslash-smoke.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cat <<"\---"
|
||||
---
|
||||
workspace:
|
||||
schema_version: 2
|
||||
\---
|
||||
EOF
|
||||
"$gate_bash" -n "$fixture/scripts/operators/double-quoted-backslash-smoke.sh"
|
||||
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/double-quoted-backslash-smoke.sh")"
|
||||
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "double-quoted backslash reviewer fixture did not execute with the Bash delimiter"
|
||||
commit_fixture script-double-quoted-backslash
|
||||
expect_rejected "double-quoted non-special backslash delimiter" "scripts/operators/double-quoted-backslash-smoke.sh"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/split-operator-smoke.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cat <\
|
||||
<'YAML'
|
||||
workspace:
|
||||
schema_version: 2
|
||||
YAML
|
||||
EOF
|
||||
"$gate_bash" -n "$fixture/scripts/operators/split-operator-smoke.sh"
|
||||
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/split-operator-smoke.sh")"
|
||||
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "split-operator reviewer fixture did not execute as a Bash heredoc"
|
||||
commit_fixture script-split-heredoc-operator
|
||||
expect_rejected "split heredoc operator continuation" "scripts/operators/split-operator-smoke.sh"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/evidence-bundle-smoke.sh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
cat <<'YAML'
|
||||
evidence:
|
||||
source: bundle
|
||||
schema_version: 2
|
||||
YAML
|
||||
EOF
|
||||
"$gate_bash" -n "$fixture/scripts/operators/evidence-bundle-smoke.sh"
|
||||
commit_fixture script-evidence-bundle
|
||||
expect_pass "evidence bundle without workspace mapping"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/powershell-comment-smoke.ps1" <<'EOF'
|
||||
# harmless PowerShell comment \
|
||||
$workspace = @'
|
||||
workspace:
|
||||
schema_version: 2
|
||||
'@
|
||||
EOF
|
||||
commit_fixture powershell-comment-v2
|
||||
expect_rejected "PowerShell comment backslash before v2 here-string" "scripts/operators/powershell-comment-smoke.ps1"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
|
||||
$workspace = @'
|
||||
EOF
|
||||
cat "$fixture/deploy/workspaces/example.yaml" >>"$fixture/scripts/operators/powershell-valid-smoke.ps1"
|
||||
cat >>"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
|
||||
'@
|
||||
$bundle = @'
|
||||
evidence:
|
||||
source: bundle
|
||||
schema_version: 2
|
||||
'@
|
||||
EOF
|
||||
commit_fixture powershell-v3-and-bundle
|
||||
expect_rejected "PowerShell embedded v3 descriptor" "scripts/operators/powershell-valid-smoke.ps1"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/operators"
|
||||
cat >"$fixture/scripts/operators/powershell-bundle-smoke.ps1" <<'EOF'
|
||||
$bundle = @'
|
||||
evidence:
|
||||
source: bundle
|
||||
schema_version: 2
|
||||
'@
|
||||
EOF
|
||||
commit_fixture powershell-bundle
|
||||
expect_pass "PowerShell non-workspace bundle"
|
||||
|
||||
# Quoted/space/indented YAML keys are real mappings; v3 passes and non-v3 fails.
|
||||
seed_fixture
|
||||
write_fixture_descriptor \
|
||||
"$fixture/deploy/workspaces/example.yaml" \
|
||||
'"workspace" :' \
|
||||
" 'schema_version' : 3"
|
||||
commit_fixture quoted-yaml-v3
|
||||
expect_pass "quoted and indented workspace v3"
|
||||
|
||||
seed_fixture
|
||||
cat >"$fixture/deploy/workspaces/example.yaml" <<'EOF'
|
||||
'workspace' :
|
||||
"schema_version" : 02
|
||||
EOF
|
||||
commit_fixture quoted-yaml-noncanonical
|
||||
expect_rejected "quoted workspace noncanonical schema" "deploy/workspaces/example.yaml"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'workspace: { schema_version: 3 }' >"$fixture/deploy/workspaces/example.yaml"
|
||||
commit_fixture inline-workspace
|
||||
expect_rejected "inline workspace mapping" "deploy/workspaces/example.yaml"
|
||||
|
||||
# Deleted migrator basenames are rejected case-insensitively at any live nesting depth.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/src/deep/nested"
|
||||
printf '%s\n' 'export const otherwiseClean = true;' >"$fixture/backend/src/deep/nested/Migrate-Legacy.ts"
|
||||
commit_fixture deleted-case-path
|
||||
expect_rejected "case-insensitive deleted basename" "backend/src/deep/nested/Migrate-Legacy.ts"
|
||||
|
||||
# Live filesystem/index trust is fail-closed, including ignored and newline-bearing files.
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const changed = true;' >"$fixture/backend/src/server.ts"
|
||||
expect_rejected "modified tracked source" "backend/src/server.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const staged = true;' >"$fixture/backend/src/server.ts"
|
||||
"$real_git" -C "$fixture" add backend/src/server.ts
|
||||
expect_rejected "staged tracked source" "backend/src/server.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'export const untracked = true;' >"$fixture/backend/src/untracked.ts"
|
||||
expect_rejected "untracked production source" "backend/src/untracked.ts"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'backend/src/ignored.ts' >"$fixture/.gitignore"
|
||||
commit_fixture ignore-rule
|
||||
printf '%s\n' 'export const ignored = true;' >"$fixture/backend/src/ignored.ts"
|
||||
expect_rejected "ignored production source" "backend/src/ignored.ts"
|
||||
|
||||
seed_fixture
|
||||
untracked_newline="$fixture/backend/src/untracked
|
||||
production.ts"
|
||||
printf '%s\n' 'export const untrackedNewline = true;' >"$untracked_newline"
|
||||
expect_rejected "newline-bearing untracked source" "backend/src/untracked\nproduction.ts"
|
||||
|
||||
seed_fixture
|
||||
ln -s server.ts "$fixture/backend/src/tracked-link.ts"
|
||||
commit_fixture tracked-symlink
|
||||
expect_rejected "tracked live symlink" "backend/src/tracked-link.ts"
|
||||
|
||||
# Generic non-workspace state/version formats remain allowed on live paths.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/scripts/nested" "$fixture/scripts/operators"
|
||||
printf '%s\n' \
|
||||
'const first = entry.state;' \
|
||||
'const second = lease.state === "operational";' \
|
||||
'const third = job.state;' >"$fixture/backend/scripts/nested/generic-state.mjs"
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'bundle_schema_version=1' >"$fixture/scripts/operators/bundle-smoke.sh"
|
||||
commit_fixture unrelated-state-version
|
||||
expect_pass "unrelated entry lease job state and bundle version"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'const stale = selectedWorkspace?.state;' >"$fixture/backend/scripts/runtime-check.mjs"
|
||||
commit_fixture workspace-state-gate
|
||||
expect_rejected "selected workspace revision state" "backend/scripts/runtime-check.mjs"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'const revision = { revision: { id: "x", state: "operational" } };' >"$fixture/backend/scripts/runtime-check.mjs"
|
||||
commit_fixture revision-object-state
|
||||
expect_rejected "bounded revision object state" "backend/scripts/runtime-check.mjs"
|
||||
|
||||
# A top-level workspace descriptor has one exact schema_version: 3 key.
|
||||
for malformed in schema-v1 schema-v2 leading-zero hexadecimal multiline duplicate; do
|
||||
seed_fixture
|
||||
case "$malformed" in
|
||||
schema-v1)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 1' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
schema-v2)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
leading-zero)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 02' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
hexadecimal)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 0x2' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
multiline)
|
||||
printf '%s\n' 'workspace:' ' schema_version: >' ' 3' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
duplicate)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 3' ' schema_version: 3' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
esac
|
||||
commit_fixture "yaml-$malformed"
|
||||
expect_rejected "malformed workspace schema $malformed" "deploy/workspaces/example.yaml"
|
||||
done
|
||||
|
||||
# YAML that is not a top-level workspace descriptor is not a generic schema-version target.
|
||||
seed_fixture
|
||||
printf '%s\n' 'bundle_schema_version: 1' >"$fixture/deploy/workspaces/preprocess-dwh.yaml"
|
||||
commit_fixture unrelated-yaml-version
|
||||
expect_pass "unrelated YAML schema version"
|
||||
|
||||
# Explicit deleted source paths and case-insensitive deleted basenames cannot hide as directories.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/src/workspaces/migrate-legacy.ts"
|
||||
expect_rejected "deleted source directory" "backend/src/workspaces/migrate-legacy.ts"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/src/deep/Migrate-V2-Qdrant.ts"
|
||||
expect_rejected "case-insensitive deleted directory" "backend/src/deep/Migrate-V2-Qdrant.ts"
|
||||
|
||||
# Test and fixture naming never bypasses trust or content policy.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/frontend/src/test"
|
||||
ln -s ../api/workspaces.ts "$fixture/frontend/src/test/negative.test.ts"
|
||||
commit_fixture tracked-test-symlink
|
||||
expect_rejected "tracked test symlink" "frontend/src/test/negative.test.ts"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/frontend/src/test"
|
||||
printf '%s\n' 'export const clean = true;' >"$fixture/frontend/src/test/changed.test.ts"
|
||||
commit_fixture tracked-test-dirty
|
||||
printf '%s\n' 'export const changed = true;' >"$fixture/frontend/src/test/changed.test.ts"
|
||||
expect_rejected "dirty test file" "frontend/src/test/changed.test.ts"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/frontend/src/test"
|
||||
printf '%s\n' 'migration_required' >"$fixture/frontend/src/test/negative.test.ts"
|
||||
commit_fixture tracked-test-forbidden
|
||||
expect_rejected "forbidden marker in test path" "frontend/src/test/negative.test.ts"
|
||||
|
||||
# Explicitly live test-named Windows and workspace fixtures are not excluded.
|
||||
seed_fixture
|
||||
printf '%s\n' 'Write-Output "MIGRATE-LEGACY"' >"$fixture/scripts/test-windows-clone-contract.ps1"
|
||||
commit_fixture live-windows-exception
|
||||
expect_rejected "live Windows fixture exception" "scripts/test-windows-clone-contract.ps1"
|
||||
|
||||
seed_fixture
|
||||
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
|
||||
commit_fixture live-workspace-fixture
|
||||
expect_rejected "live workspace fixture exception" "scripts/fixtures/workspace-registry-smoke.yaml"
|
||||
|
||||
seed_fixture
|
||||
write_fixture_descriptor \
|
||||
"$fixture/scripts/fixtures/workspace-registry-future.yaml" \
|
||||
'workspace:' \
|
||||
' schema_version: 2'
|
||||
commit_fixture future-workspace-family
|
||||
expect_rejected "future workspace fixture family" "scripts/fixtures/workspace-registry-future.yaml"
|
||||
|
||||
# Only exact path+category policy literals are allowed; paths outside policy roots remain out of scope.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/docs/superpowers/plans"
|
||||
printf '%s\n' 'Historical schema_version: 2 and migration_required.' >"$fixture/docs/superpowers/plans/history.md"
|
||||
printf '%s\n' 'migration_required migrate-legacy WorkspaceV2 revision.state' >"$fixture/scripts/test-verify-schema-v3-only.sh"
|
||||
commit_fixture exact-policy-allowlist
|
||||
expect_pass "exact self-test policy allowlist and historical docs"
|
||||
|
||||
# Diagnostic paths are shell-escaped so a newline cannot forge another log line.
|
||||
seed_fixture
|
||||
newline_spoof="$fixture/backend/src/spoof
|
||||
forged.py"
|
||||
printf '%s\n' harmless >"$newline_spoof"
|
||||
run_gate
|
||||
[[ $gate_status -eq 1 ]] || fail "newline untracked path was not rejected"
|
||||
grep -Fq 'backend/src/spoof\nforged.py' "$output" \
|
||||
|| fail "newline path diagnostic was not escaped on one line"
|
||||
|
||||
# Scanner operational errors are propagated, not converted into absence.
|
||||
seed_fixture
|
||||
fake_bin="$sandbox/fake-bin"
|
||||
mkdir -p "$fake_bin"
|
||||
cat >"$fake_bin/git" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
for argument in "$@"; do
|
||||
if [[ "$argument" == grep ]]; then
|
||||
echo "simulated git grep failure" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
exec "$REAL_GIT" "$@"
|
||||
EOF
|
||||
chmod +x "$fake_bin/git"
|
||||
set +e
|
||||
PATH="$fake_bin:$PATH" REAL_GIT="$real_git" "$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
|
||||
gate_status=$?
|
||||
set -e
|
||||
[[ $gate_status -eq 2 ]] || fail "git grep status 2 was masked as $gate_status"
|
||||
grep -Fq 'simulated git grep failure' "$output" || fail "git grep failure diagnostics were lost"
|
||||
|
||||
# Foreign roots are test-only and can never select fixture code for a full check.
|
||||
set +e
|
||||
"$gate_bash" "$gate" --root "$fixture" >"$output" 2>&1
|
||||
gate_status=$?
|
||||
set -e
|
||||
[[ $gate_status -ne 0 ]] || fail "foreign-root full mode unexpectedly passed"
|
||||
grep -Fq -- '--root is available only with --runtime-only or --bootstrap-trust-only' "$output" \
|
||||
|| fail "foreign-root full rejection did not report the trust boundary"
|
||||
|
||||
# Prescribed symbols are forbidden as case-insensitive substrings, including derivatives.
|
||||
derivative_names=(WorkspaceV2Compat wOrKsPaCeV2 deprecatedV2Descriptor WRITEMIGRATEDWORKSPACE LegacyWorkspaceAdapter migrateLegacyWorkspaceCompat)
|
||||
for derivative in "${derivative_names[@]}"; do
|
||||
for matrix_root in "${policy_roots[@]}"; do
|
||||
seed_fixture
|
||||
matrix_path="$matrix_root/derivative.ts"
|
||||
printf '%s\n' "$derivative" >"$fixture/$matrix_path"
|
||||
commit_fixture "derivative-$derivative-${matrix_root//\//-}"
|
||||
expect_rejected "derivative $derivative in $matrix_root" "$matrix_path"
|
||||
done
|
||||
done
|
||||
|
||||
# Mixed/all-lower legacy spellings fail; the approved lower-camel identifier remains valid.
|
||||
for spelling in legacyworkspace LeGaCyWoRkSpAcE; do
|
||||
seed_fixture
|
||||
printf '%s\n' "$spelling" >"$fixture/backend/src/legacy-variant.ts"
|
||||
commit_fixture legacy-spelling
|
||||
expect_rejected "legacy spelling $spelling" "backend/src/legacy-variant.ts"
|
||||
done
|
||||
seed_fixture
|
||||
printf '%s\n' 'const legacyWorkspacePath = current;' >"$fixture/backend/src/legacy-allowed.ts"
|
||||
commit_fixture lower-camel-legacy
|
||||
expect_pass "approved lower-camel legacy identifier"
|
||||
|
||||
# Full-text revision scanning covers bracket access and newline-separated dot access.
|
||||
for revision_source in \
|
||||
'selectedWorkspace["state"]' \
|
||||
$'workspaceRevision\n .state'; do
|
||||
seed_fixture
|
||||
printf '%s\n' "$revision_source" >"$fixture/frontend/src/revision-variant.ts"
|
||||
commit_fixture revision-variant
|
||||
expect_rejected "revision structural variant" "frontend/src/revision-variant.ts"
|
||||
done
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/src/workspaces"
|
||||
printf '%s\n' 'if (revision.state !== "operational") return;' >"$fixture/backend/src/workspaces/registry.ts"
|
||||
commit_fixture historical-decoder
|
||||
expect_pass "single exact historical decoder"
|
||||
printf '%s\n' 'if (revision["state"] === "retired") return;' >>"$fixture/backend/src/workspaces/registry.ts"
|
||||
commit_fixture extra-historical-branch
|
||||
expect_rejected "extra registry revision branch" "backend/src/workspaces/registry.ts"
|
||||
|
||||
# Binary/NUL policy files are decoded and rejected rather than skipped by git grep -I.
|
||||
seed_fixture
|
||||
printf 'WorkspaceV2\0hidden\n' >"$fixture/backend/src/binary.ts"
|
||||
commit_fixture nul-policy
|
||||
expect_rejected "NUL policy file" "backend/src/binary.ts"
|
||||
|
||||
# Workspace fixture-family discovery is recursive by basename.
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/scripts/fixtures/nested/deeper"
|
||||
write_fixture_descriptor "$fixture/scripts/fixtures/nested/deeper/workspace-registry-nested.yaml" workspace: ' schema_version: 2'
|
||||
commit_fixture nested-workspace-fixture
|
||||
expect_rejected "nested workspace fixture" "scripts/fixtures/nested/deeper/workspace-registry-nested.yaml"
|
||||
|
||||
# Python bytecode is disabled before pre-gate docs, and release dry-run starts with bootstrap trust.
|
||||
grep -Fq 'PYTHONDONTWRITEBYTECODE: "1"' "$project_root/.github/workflows/deployment.yml" \
|
||||
|| fail "workflow does not disable Python bytecode"
|
||||
grep -Fq 'export PYTHONDONTWRITEBYTECODE=1' "$project_root/scripts/verify-schema-v3-only-release.sh" \
|
||||
|| fail "release wrapper does not disable Python bytecode"
|
||||
release_plan="$($gate_bash "$project_root/scripts/verify-schema-v3-only-release.sh" --dry-run)"
|
||||
first_command="$(printf '%s\n' "$release_plan" | sed -n '1p')"
|
||||
bootstrap_command="$(printf '%s\n' "$release_plan" | sed -n '4p')"
|
||||
[[ "$first_command" == 'export PYTHONDONTWRITEBYTECODE=1' ]] \
|
||||
|| fail "release dry-run does not print the Python bytecode export"
|
||||
[[ "$bootstrap_command" == '/bin/bash scripts/verify-schema-v3-only.sh --bootstrap-trust-only' ]] \
|
||||
|| fail "release plan does not bootstrap trust before npm"
|
||||
printf '%s\n' "$release_plan" | grep -Fq '(cd backend && npm ci --ignore-scripts)' \
|
||||
|| fail "release plan does not disable npm lifecycle scripts"
|
||||
py_fixture="$sandbox/python-bytecode"
|
||||
mkdir -p "$py_fixture/scripts"
|
||||
printf '%s\n' 'value = 3' >"$py_fixture/scripts/module.py"
|
||||
PYTHONPATH="$py_fixture" PYTHONDONTWRITEBYTECODE=1 python3 -c 'import scripts.module'
|
||||
[[ ! -e "$py_fixture/scripts/__pycache__" ]] || fail "pre-gate Python created ignored bytecode"
|
||||
|
||||
seed_bootstrap_fixture() {
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/.github/workflows"
|
||||
for required in \
|
||||
backend/package.json backend/package-lock.json \
|
||||
backend/scripts/verify-workspace-descriptor-files.mjs \
|
||||
backend/scripts/verify-workspace-descriptor-files.test.mjs \
|
||||
backend/scripts/revision-state-policy.mjs \
|
||||
backend/scripts/revision-state-policy.test.mjs \
|
||||
backend/scripts/bash-heredoc.mjs \
|
||||
backend/scripts/revision_state_policy.py \
|
||||
backend/scripts/test_revision_state_policy.py \
|
||||
scripts/verify-schema-v3-only.sh scripts/test-verify-schema-v3-only.sh \
|
||||
scripts/verify-schema-v3-only-release.sh scripts/workspace_descriptor_doc_contract.py \
|
||||
.github/workflows/deployment.yml; do
|
||||
mkdir -p "$fixture/${required%/*}"
|
||||
cp "$project_root/$required" "$fixture/$required"
|
||||
done
|
||||
"$real_git" -C "$fixture" add .
|
||||
"$real_git" -C "$fixture" commit -qm bootstrap-files
|
||||
}
|
||||
assert_release_stops_before_npm() {
|
||||
local label="$1"
|
||||
fake_lifecycle="$sandbox/fake-lifecycle"
|
||||
mkdir -p "$fake_lifecycle"
|
||||
cat >"$fake_lifecycle/npm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
echo invoked >>"$sandbox/npm-invoked"
|
||||
exit 99
|
||||
EOF
|
||||
chmod +x "$fake_lifecycle/npm"
|
||||
rm -f "$sandbox/npm-invoked"
|
||||
set +e
|
||||
PATH="$fake_lifecycle:$PATH" /bin/bash "$fixture/scripts/verify-schema-v3-only-release.sh" >"$output" 2>&1
|
||||
release_status=$?
|
||||
set -e
|
||||
[[ $release_status -ne 0 ]] || fail "$label unexpectedly passed"
|
||||
[[ ! -e "$sandbox/npm-invoked" ]] || fail "$label invoked npm before bootstrap trust"
|
||||
}
|
||||
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' '# dirty' >>"$fixture/backend/package.json"
|
||||
assert_release_stops_before_npm "dirty package bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/verify-workspace-descriptor-files.test.mjs"
|
||||
assert_release_stops_before_npm "dirty checker test bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' '// dirty' >>"$fixture/backend/scripts/revision-state-policy.mjs"
|
||||
assert_release_stops_before_npm "dirty revision policy bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/revision_state_policy.py"
|
||||
assert_release_stops_before_npm "dirty Python policy helper bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' '# dirty' >>"$fixture/scripts/verify-schema-v3-only.sh"
|
||||
assert_release_stops_before_npm "dirty gate bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
rm "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
|
||||
ln -s /dev/null "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
|
||||
assert_release_stops_before_npm "symlink checker bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' 'scripts/__pycache__/' >"$fixture/.gitignore"
|
||||
"$real_git" -C "$fixture" add .gitignore
|
||||
"$real_git" -C "$fixture" commit -qm ignore-rule
|
||||
mkdir -p "$fixture/scripts/__pycache__"
|
||||
printf x >"$fixture/scripts/__pycache__/ignored.pyc"
|
||||
assert_release_stops_before_npm "ignored trusted artifact bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf x >"$fixture/scripts/untracked-helper.sh"
|
||||
assert_release_stops_before_npm "untracked helper bootstrap"
|
||||
|
||||
seed_bootstrap_fixture
|
||||
mkfifo "$fixture/scripts/bootstrap-fifo"
|
||||
assert_release_stops_before_npm "FIFO bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
|
||||
assert_release_stops_before_npm "untracked backend npmrc bootstrap"
|
||||
seed_bootstrap_fixture
|
||||
global_ignore="$sandbox/global-ignore"
|
||||
printf '%s\n' backend/.npmrc >"$global_ignore"
|
||||
"$real_git" -C "$fixture" config core.excludesFile "$global_ignore"
|
||||
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
|
||||
assert_release_stops_before_npm "globally ignored backend npmrc bootstrap"
|
||||
|
||||
# Dist failures are isolated to fixture roots; canonical backend/dist is never mutated.
|
||||
run_gate_dist() {
|
||||
set +e
|
||||
"$gate_bash" "$gate" --root "$fixture" --runtime-only --check-dist >"$output" 2>&1
|
||||
gate_status=$?
|
||||
set -e
|
||||
}
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/dist"
|
||||
printf '%s\n' server >"$fixture/backend/dist/server.js"
|
||||
run_gate_dist
|
||||
[[ $gate_status -eq 1 ]] || fail "fixture missing schema module unexpectedly passed"
|
||||
grep -Fq 'backend/dist/workspaces/schema.js' "$output" || fail "fixture missing schema path not reported"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/dist/workspaces"
|
||||
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
|
||||
run_gate_dist
|
||||
[[ $gate_status -eq 1 ]] || fail "fixture missing server unexpectedly passed"
|
||||
grep -Fq 'backend/dist/server.js' "$output" || fail "fixture missing server path not reported"
|
||||
|
||||
seed_fixture
|
||||
mkdir -p "$fixture/backend/dist/workspaces"
|
||||
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
|
||||
printf '%s\n' server >"$fixture/backend/dist/server.js"
|
||||
printf '%s\n' stale >"$fixture/backend/dist/workspaces/Migrate-Legacy.js"
|
||||
run_gate_dist
|
||||
[[ $gate_status -eq 1 ]] || fail "fixture stale migrator unexpectedly passed"
|
||||
grep -Fq 'backend/dist/workspaces/Migrate-Legacy.js' "$output" || fail "fixture stale migrator path not reported"
|
||||
|
||||
[[ "$(cksum <"$canonical_schema")" == "$canonical_schema_checksum" ]] \
|
||||
|| fail "shell regression mutated canonical compiled schema"
|
||||
[[ "$(cksum <"$canonical_server")" == "$canonical_server_checksum" ]] \
|
||||
|| fail "shell regression mutated canonical compiled server"
|
||||
|
||||
echo "schema-v3-only absence gate regression tests passed"
|
||||
@@ -478,31 +478,8 @@ task13_seed_registry() {
|
||||
task13_run_logged "initialize bare workspace registry" \
|
||||
git init --bare --initial-branch=main "$TASK13_REMOTE"
|
||||
task13_run_logged "initialize workspace seed" git -C "$TASK13_SEED" init --initial-branch=main
|
||||
cat >"$TASK13_SEED/workspaces/task13-smoke.yaml" <<'EOF'
|
||||
workspace:
|
||||
schema_version: 3
|
||||
id: task13-smoke
|
||||
name: Task 13 Smoke
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: analytics
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: task13-smoke
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
default: local-qwen/task13-smoke
|
||||
allowed: [local-qwen/task13-smoke]
|
||||
EOF
|
||||
cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" \
|
||||
"$TASK13_SEED/workspaces/task13-smoke.yaml"
|
||||
task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" add workspaces/task13-smoke.yaml
|
||||
task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" \
|
||||
-c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' \
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducible schema-v3-only release gate. The Git checkout is the trust root;
|
||||
# dependencies come from backend/package-lock.json and dist comes from a clean build.
|
||||
set -euo pipefail
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export NPM_CONFIG_USERCONFIG=/dev/null
|
||||
export NPM_CONFIG_GLOBALCONFIG=/dev/null
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
if [[ ${1:-} == --dry-run ]]; then
|
||||
cat <<'EOF'
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export NPM_CONFIG_USERCONFIG=/dev/null
|
||||
export NPM_CONFIG_GLOBALCONFIG=/dev/null
|
||||
/bin/bash scripts/verify-schema-v3-only.sh --bootstrap-trust-only
|
||||
(cd backend && npm ci --ignore-scripts)
|
||||
(cd backend && npm run build)
|
||||
(cd backend && npm run test:schema-v3-verifier)
|
||||
/bin/bash scripts/test-verify-schema-v3-only.sh
|
||||
/bin/bash scripts/verify-schema-v3-only.sh
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
[[ $# -eq 0 ]] || { echo "usage: $0 [--dry-run]" >&2; exit 2; }
|
||||
/bin/bash "$root/scripts/verify-schema-v3-only.sh" --bootstrap-trust-only
|
||||
(cd "$root/backend" && npm ci --ignore-scripts)
|
||||
(cd "$root/backend" && npm run build)
|
||||
(cd "$root/backend" && npm run test:schema-v3-verifier)
|
||||
/bin/bash "$root/scripts/test-verify-schema-v3-only.sh"
|
||||
/bin/bash "$root/scripts/verify-schema-v3-only.sh"
|
||||
Executable
+278
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fail-closed absence gate for the supported schema-v3-only workspace runtime.
|
||||
set -euo pipefail
|
||||
shopt -s nocasematch
|
||||
|
||||
script_root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
root_argument="$script_root"
|
||||
root_was_selected=0
|
||||
runtime_only=0
|
||||
check_dist=0
|
||||
bootstrap_trust_only=0
|
||||
|
||||
usage() {
|
||||
cat >&2 <<EOF
|
||||
usage: $0 [--bootstrap-trust-only [--root REPOSITORY]]
|
||||
$0 [--runtime-only [--root REPOSITORY] [--check-dist]]
|
||||
|
||||
Default mode trusts the canonical Git checkout, runs a clean backend build, and
|
||||
then validates generated output and documentation. Runtime-only uses the trusted
|
||||
canonical Node installation, dependencies, verifier, and built
|
||||
backend/dist/workspaces/schema.js; --root is only for isolated Git fixtures.
|
||||
--check-dist makes an isolated runtime fixture provide backend/dist/schema.js and
|
||||
server.js and checks it for stale migrators. Full mode never accepts overrides.
|
||||
Expandable deployment blocks are trusted only by repository-relative path plus the
|
||||
SHA-256 of their exact raw opener/body/closer bytes in the Node verifier. This is
|
||||
an exact-content trust exception with rationale metadata, not semantic proof.
|
||||
|
||||
Release trust anchor and order (durable entry point):
|
||||
Git index/filesystem trust is established by --bootstrap-trust-only before any
|
||||
checkout-controlled helper or npm lifecycle can run. CI supplies a clean Git
|
||||
checkout and performs an inline clean-checkout assertion before the wrapper.
|
||||
scripts/verify-schema-v3-only-release.sh then runs npm ci --ignore-scripts from the trusted
|
||||
backend/package-lock.json; clean build; npm Node verifier test; Bash regression;
|
||||
and the full schema-v3-only gate, which repeats trust checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--root) [[ $# -ge 2 ]] || { usage; exit 2; }; root_argument="$2"; root_was_selected=1; shift 2 ;;
|
||||
--runtime-only) runtime_only=1; shift ;;
|
||||
--check-dist) check_dist=1; shift ;;
|
||||
--bootstrap-trust-only) bootstrap_trust_only=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ $root_was_selected -eq 0 || $runtime_only -eq 1 || $bootstrap_trust_only -eq 1 ]] || { echo "--root is available only with --runtime-only or --bootstrap-trust-only" >&2; exit 2; }
|
||||
[[ $runtime_only -eq 0 || $bootstrap_trust_only -eq 0 ]] || { echo "--runtime-only and --bootstrap-trust-only are mutually exclusive" >&2; exit 2; }
|
||||
[[ $check_dist -eq 0 || $runtime_only -eq 1 ]] || { echo "--check-dist is available only with --runtime-only" >&2; exit 2; }
|
||||
if ! root="$(cd "$root_argument" 2>/dev/null && pwd -P)"; then
|
||||
printf 'repository root is not accessible: %q\n' "$root_argument" >&2
|
||||
exit 2
|
||||
fi
|
||||
[[ $runtime_only -eq 1 || $bootstrap_trust_only -eq 1 || "$root" == "$script_root" ]] || { echo "full mode is restricted to the canonical repository" >&2; exit 2; }
|
||||
|
||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v3-gate.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
if git_top="$(git -C "$root" rev-parse --show-toplevel 2>"$tmp/rev-parse")"; then :; else
|
||||
status=$?; printf 'Git repository discovery failed for %q\n' "$root" >&2; cat "$tmp/rev-parse" >&2; exit "$status"
|
||||
fi
|
||||
canonical_git_top="$(cd "$git_top" && pwd -P)"
|
||||
[[ "$canonical_git_top" == "$root" ]] || { printf '%s\n' "--root must name the canonical Git root" >&2; exit 2; }
|
||||
for npmrc in .npmrc backend/.npmrc; do
|
||||
if [[ -e "$root/$npmrc" || -L "$root/$npmrc" ]]; then
|
||||
printf 'npm configuration node forbidden: %q\n' "$npmrc" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
policy_roots=(backend/src frontend/src backend/scripts scripts)
|
||||
trust_roots=(backend/src frontend/src backend/scripts scripts deploy/workspaces)
|
||||
|
||||
print_path() { printf '%q' "$1"; }
|
||||
fail_path() { local message="$1" path="$2"; printf '%s: ' "$message" >&2; print_path "$path" >&2; printf '\n' >&2; return 1; }
|
||||
|
||||
forbidden_module_stems='deprecated-v2-descriptor|migrate-legacy|migrate-v2-qdrant'
|
||||
is_deleted_basename() {
|
||||
[[ "${1##*/}" =~ ^($forbidden_module_stems)(\..*)?$ ]]
|
||||
}
|
||||
|
||||
# Exact path + category exceptions only. They remain fully subject to trust checks.
|
||||
is_allowed_match() {
|
||||
local category="$1" path="$2"
|
||||
case "$category:$path" in
|
||||
prescribed-symbol:scripts/verify-schema-v3-only.sh|prescribed-symbol:scripts/test-verify-schema-v3-only.sh|prescribed-symbol:backend/scripts/verify-workspace-descriptor-files.mjs|prescribed-symbol:backend/scripts/verify-workspace-descriptor-files.test.mjs|prescribed-symbol:backend/scripts/revision-state-policy.mjs|prescribed-symbol:backend/scripts/revision-state-policy.test.mjs|prescribed-symbol:backend/scripts/bash-heredoc.mjs|prescribed-symbol:backend/scripts/revision_state_policy.py|prescribed-symbol:backend/scripts/test_revision_state_policy.py) return 0 ;;
|
||||
legacy-workspace:scripts/verify-schema-v3-only.sh|legacy-workspace:scripts/test-verify-schema-v3-only.sh|legacy-workspace:backend/scripts/verify-workspace-descriptor-files.mjs|legacy-workspace:backend/scripts/verify-workspace-descriptor-files.test.mjs|legacy-workspace:backend/scripts/revision-state-policy.mjs|legacy-workspace:backend/scripts/revision-state-policy.test.mjs|legacy-workspace:backend/scripts/bash-heredoc.mjs|legacy-workspace:backend/scripts/revision_state_policy.py|legacy-workspace:backend/scripts/test_revision_state_policy.py) return 0 ;;
|
||||
migration-marker:scripts/verify-schema-v3-only.sh|migration-marker:scripts/test-verify-schema-v3-only.sh|migration-marker:backend/scripts/verify-workspace-descriptor-files.mjs|migration-marker:backend/scripts/verify-workspace-descriptor-files.test.mjs|migration-marker:backend/scripts/revision-state-policy.mjs|migration-marker:backend/scripts/revision-state-policy.test.mjs|migration-marker:backend/scripts/bash-heredoc.mjs|migration-marker:backend/scripts/revision_state_policy.py|migration-marker:backend/scripts/test_revision_state_policy.py) return 0 ;;
|
||||
migration-marker:scripts/workspace_descriptor_doc_contract.py|migration-marker:scripts/test_workspace_descriptor_doc_contract.py) return 0 ;;
|
||||
migration-marker:backend/scripts/clean-dist.test.mjs) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Common policy is defined once and scanned over every policy root. Revision-state is the sole layer.
|
||||
prescribed_symbol_forbidden='WorkspaceV1|WorkspaceV2|DeprecatedV2Descriptor|LegacyMigrationResult|LegacyMigrationOptions|WorkspaceV2MigrationInput|migrateLegacyWorkspace|writeMigratedWorkspace|migrateWorkspaceV1ToV2|migrateWorkspaceV2ToV3'
|
||||
legacy_workspace_forbidden='LegacyWorkspace'
|
||||
migration_marker_forbidden="migration_required|($forbidden_module_stems)"
|
||||
|
||||
require_category_absent() {
|
||||
local category="$1" sensitivity="$2" pattern="$3" output="$tmp/grep-$1" status path
|
||||
if [[ "$sensitivity" == insensitive ]]; then
|
||||
if git -C "$root" grep -z -l -I -i -E -e "$pattern" -- "${policy_roots[@]}" >"$output" 2>"$output.err"; then status=0; else status=$?; fi
|
||||
else
|
||||
if git -C "$root" grep -z -l -I -E -e "$pattern" -- "${policy_roots[@]}" >"$output" 2>"$output.err"; then status=0; else status=$?; fi
|
||||
fi
|
||||
case "$status" in
|
||||
0)
|
||||
while IFS= read -r -d '' path; do
|
||||
is_allowed_match "$category" "$path" && continue
|
||||
printf 'forbidden %s match: ' "$category" >&2; print_path "$path" >&2; printf '\n' >&2
|
||||
return 1
|
||||
done <"$output"
|
||||
;;
|
||||
1) : ;;
|
||||
*) printf 'scanner failure (%s): %s\n' "$status" "$category" >&2; cat "$output.err" >&2; return "$status" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# One batched index inventory validates modes and working-tree presence for all tracked paths.
|
||||
index_entries="$tmp/index"
|
||||
if git -C "$root" ls-files -s -z -- "${trust_roots[@]}" >"$index_entries" 2>"$tmp/index.err"; then :; else
|
||||
status=$?; echo "tracked index scan failed ($status)" >&2; cat "$tmp/index.err" >&2; exit "$status"
|
||||
fi
|
||||
tracked_paths="$tmp/tracked"
|
||||
: >"$tracked_paths"
|
||||
while IFS= read -r -d '' record; do
|
||||
metadata="${record%%$'\t'*}"; path="${record#*$'\t'}"; mode="${metadata%% *}"
|
||||
case "$mode" in 100*) ;; 120000) fail_path "tracked symlink forbidden" "$path"; exit 1 ;; *) fail_path "non-regular tracked entry forbidden" "$path"; exit 1 ;; esac
|
||||
printf '%s\0' "$path" >>"$tracked_paths"
|
||||
[[ -f "$root/$path" && ! -L "$root/$path" ]] || { fail_path "tracked file missing or unsafe" "$path"; exit 1; }
|
||||
is_deleted_basename "$path" && { fail_path "deleted source basename remains tracked" "$path"; exit 1; }
|
||||
done <"$index_entries"
|
||||
|
||||
# Filesystem node trust has no test/fixture exclusions.
|
||||
filesystem="$tmp/filesystem"
|
||||
if find "${trust_roots[@]/#/$root/}" -mindepth 1 -print0 >"$filesystem" 2>"$tmp/find.err"; then :; else
|
||||
status=$?; echo "filesystem trust scan failed ($status)" >&2; cat "$tmp/find.err" >&2; exit "$status"
|
||||
fi
|
||||
while IFS= read -r -d '' absolute; do
|
||||
path="${absolute#"$root/"}"
|
||||
[[ ! -L "$absolute" ]] || { fail_path "symlink forbidden in trusted root" "$path"; exit 1; }
|
||||
[[ -d "$absolute" || -f "$absolute" ]] || { fail_path "non-directory/non-regular node forbidden in trusted root" "$path"; exit 1; }
|
||||
is_deleted_basename "$path" && { fail_path "deleted source basename remains on filesystem" "$path"; exit 1; }
|
||||
done <"$filesystem"
|
||||
|
||||
reject_name_list() {
|
||||
local label="$1" file="$2" path
|
||||
while IFS= read -r -d '' path; do fail_path "$label" "$path"; return 1; done <"$file"
|
||||
}
|
||||
for spec in "untracked:--others --exclude-standard" "ignored:--others --ignored --exclude-standard"; do
|
||||
label="${spec%%:*}"; options="${spec#*:}"; output="$tmp/$label"
|
||||
# shellcheck disable=SC2086
|
||||
if git -C "$root" ls-files -z $options -- "${trust_roots[@]}" >"$output" 2>"$output.err"; then :; else
|
||||
status=$?; echo "$label scan failed ($status)" >&2; cat "$output.err" >&2; exit "$status"
|
||||
fi
|
||||
reject_name_list "$label file in trusted root" "$output" || exit 1
|
||||
done
|
||||
|
||||
for mode in worktree cached; do
|
||||
output="$tmp/dirty-$mode"
|
||||
if [[ "$mode" == cached ]]; then command=(git -C "$root" diff --cached --name-only -z --); else command=(git -C "$root" diff --name-only -z --); fi
|
||||
if "${command[@]}" "${trust_roots[@]}" >"$output" 2>"$output.err"; then :; else
|
||||
status=$?; echo "$mode dirty scan failed ($status)" >&2; cat "$output.err" >&2; exit "$status"
|
||||
fi
|
||||
reject_name_list "modified trusted file ($mode)" "$output" || exit 1
|
||||
done
|
||||
|
||||
require_trusted_files_at() {
|
||||
local repository="$1"; shift
|
||||
local listing="$tmp/explicit-$RANDOM" record metadata path mode expected
|
||||
if git -C "$repository" ls-files -s -z -- "$@" >"$listing" 2>"$listing.err"; then :; else
|
||||
status=$?; echo "explicit trust index scan failed ($status)" >&2; cat "$listing.err" >&2; return "$status"
|
||||
fi
|
||||
: >"$listing.paths"
|
||||
while IFS= read -r -d '' record; do
|
||||
metadata="${record%%$'\t'*}"; path="${record#*$'\t'}"; mode="${metadata%% *}"
|
||||
case "$mode" in 100*) ;; *) fail_path "required file is not regular in index" "$path"; return 1 ;; esac
|
||||
printf '%s\n' "$path" >>"$listing.paths"
|
||||
done <"$listing"
|
||||
for expected in "$@"; do
|
||||
[[ -f "$repository/$expected" && ! -L "$repository/$expected" ]] || { fail_path "required trusted file missing or unsafe" "$expected"; return 1; }
|
||||
grep -Fqx -- "$expected" "$listing.paths" || { fail_path "required file is not tracked" "$expected"; return 1; }
|
||||
done
|
||||
git -C "$repository" diff --quiet -- "$@" || { echo "required trust files are dirty" >&2; return 1; }
|
||||
git -C "$repository" diff --quiet --cached -- "$@" || { echo "required trust files are staged dirty" >&2; return 1; }
|
||||
}
|
||||
|
||||
# Bootstrap uses only Git/filesystem primitives. It must precede every npm or
|
||||
# checkout-controlled helper in the durable release wrapper.
|
||||
bootstrap_files=(
|
||||
backend/package.json backend/package-lock.json
|
||||
backend/scripts/verify-workspace-descriptor-files.mjs
|
||||
backend/scripts/verify-workspace-descriptor-files.test.mjs
|
||||
backend/scripts/revision-state-policy.mjs
|
||||
backend/scripts/revision-state-policy.test.mjs
|
||||
backend/scripts/bash-heredoc.mjs
|
||||
backend/scripts/revision_state_policy.py
|
||||
backend/scripts/test_revision_state_policy.py
|
||||
scripts/verify-schema-v3-only.sh scripts/test-verify-schema-v3-only.sh
|
||||
scripts/verify-schema-v3-only-release.sh .github/workflows/deployment.yml
|
||||
scripts/workspace_descriptor_doc_contract.py
|
||||
)
|
||||
if [[ $bootstrap_trust_only -eq 1 ]]; then
|
||||
require_trusted_files_at "$root" "${bootstrap_files[@]}"
|
||||
echo "schema-v3-only bootstrap trust passed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Runtime fixtures execute only canonical trusted verifier code and dependencies.
|
||||
require_trusted_files_at "$script_root" \
|
||||
backend/scripts/verify-workspace-descriptor-files.mjs \
|
||||
backend/scripts/revision-state-policy.mjs \
|
||||
backend/scripts/bash-heredoc.mjs \
|
||||
backend/scripts/revision_state_policy.py \
|
||||
backend/package.json backend/package-lock.json \
|
||||
scripts/verify-schema-v3-only.sh
|
||||
workspace_verifier="$script_root/backend/scripts/verify-workspace-descriptor-files.mjs"
|
||||
workspace_schema="$script_root/backend/dist/workspaces/schema.js"
|
||||
|
||||
if [[ $runtime_only -eq 0 ]]; then
|
||||
require_trusted_files_at "$root" \
|
||||
scripts/verify-schema-v3-only-release.sh .github/workflows/deployment.yml
|
||||
(cd "$root/backend" && npm run build)
|
||||
fi
|
||||
[[ -f "$workspace_schema" && ! -L "$workspace_schema" ]] || { echo "trusted compiled schema module is missing or unsafe: backend/dist/workspaces/schema.js" >&2; exit 1; }
|
||||
|
||||
workspace_manifest="$tmp/workspace-manifest"
|
||||
: >"$workspace_manifest"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in
|
||||
backend/src/*|frontend/src/*|backend/scripts/*|scripts/*) printf '%s\0%s\0' policy_text "$path" >>"$workspace_manifest" ;;
|
||||
esac
|
||||
kind=""
|
||||
case "$path" in
|
||||
deploy/workspaces/preprocess-dwh.yaml|deploy/workspaces/preprocess-evidence.yaml|deploy/workspaces/server-sessions.yaml.example) ;;
|
||||
deploy/workspaces/*.yaml|deploy/workspaces/*.yml|deploy/workspaces/*.yaml.example|deploy/workspaces/*.yml.example|scripts/fixtures/workspace-registry-*.yaml|scripts/fixtures/workspace-registry-*.yml|scripts/fixtures/*/workspace-registry-*.yaml|scripts/fixtures/*/workspace-registry-*.yml) kind=workspace_descriptor ;;
|
||||
scripts/*.sh|scripts/*.ps1)
|
||||
case "$path" in scripts/verify-schema-v3-only.sh|scripts/test-verify-schema-v3-only.sh) ;; *) kind=deployment_script ;; esac
|
||||
;;
|
||||
esac
|
||||
[[ -z "$kind" ]] || printf '%s\0%s\0' "$kind" "$path" >>"$workspace_manifest"
|
||||
done <"$tracked_paths"
|
||||
node "$workspace_verifier" --root "$root" --manifest "$workspace_manifest"
|
||||
|
||||
require_category_absent prescribed-symbol insensitive "$prescribed_symbol_forbidden"
|
||||
require_category_absent legacy-workspace sensitive "$legacy_workspace_forbidden"
|
||||
require_category_absent migration-marker insensitive "$migration_marker_forbidden"
|
||||
|
||||
check_dist_tree() {
|
||||
local dist_root="$1" entries="$tmp/dist" absolute path
|
||||
[[ -f "$dist_root/backend/dist/workspaces/schema.js" && ! -L "$dist_root/backend/dist/workspaces/schema.js" ]] || { echo "expected compiled schema module is missing or unsafe: backend/dist/workspaces/schema.js" >&2; return 1; }
|
||||
[[ -f "$dist_root/backend/dist/server.js" && ! -L "$dist_root/backend/dist/server.js" ]] || { echo "expected compiled backend output is missing or unsafe: backend/dist/server.js" >&2; return 1; }
|
||||
find "$dist_root/backend/dist" -mindepth 1 -print0 >"$entries"
|
||||
while IFS= read -r -d '' absolute; do
|
||||
path="${absolute#"$dist_root/"}"
|
||||
if is_deleted_basename "$path"; then
|
||||
fail_path "stale compiled workspace migrator output exists" "$path"
|
||||
return 1
|
||||
fi
|
||||
done <"$entries"
|
||||
}
|
||||
[[ $runtime_only -eq 1 && $check_dist -eq 0 ]] || check_dist_tree "$root"
|
||||
|
||||
if [[ $runtime_only -eq 0 ]]; then
|
||||
require_trusted_files_at "$root" \
|
||||
scripts/workspace_descriptor_doc_contract.py README.md PROJECT_STATE.md \
|
||||
docs/install/local-workspace-registry.md docs/install/server-workspace-registry.md \
|
||||
docs/workspace-diagnostic-protocol.md
|
||||
"$root/scripts/workspace_descriptor_doc_contract.py" \
|
||||
--document "$root/README.md" --project-state "$root/PROJECT_STATE.md" \
|
||||
--document "$root/docs/install/local-workspace-registry.md" \
|
||||
--document "$root/docs/install/server-workspace-registry.md" \
|
||||
--document "$root/docs/workspace-diagnostic-protocol.md"
|
||||
fi
|
||||
|
||||
echo "schema-v3-only absence gate passed"
|
||||
Reference in New Issue
Block a user