feat: unify installation model catalog
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: local
|
||||
name: Local
|
||||
description: Isolated workspace registry smoke fixture.
|
||||
@@ -15,22 +15,6 @@ dwh:
|
||||
- postgres_direct
|
||||
- rest_api
|
||||
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: local
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
|
||||
llm_policy:
|
||||
default: zai/glm-5.2
|
||||
allowed:
|
||||
- zai/glm-5.2
|
||||
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: task13-smoke
|
||||
name: Task 13 Smoke
|
||||
language: en
|
||||
@@ -11,22 +11,6 @@ dwh:
|
||||
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
|
||||
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: task13-windows
|
||||
name: Task 13 Windows
|
||||
language: en
|
||||
@@ -14,22 +14,6 @@ dwh:
|
||||
- postgres_direct
|
||||
- rest_api
|
||||
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: task13-windows
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
|
||||
llm_policy:
|
||||
default: zai/glm-5.2
|
||||
allowed:
|
||||
- zai/glm-5.2
|
||||
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
|
||||
@@ -86,26 +86,11 @@ for (const name of [
|
||||
}
|
||||
|
||||
if (workspace.workspace?.id !== "task13-smoke") throw new Error("fixture workspace id changed");
|
||||
if (workspace.semantic_index?.vector_store?.engine !== "qdrant") {
|
||||
throw new Error("fixture workspace must use qdrant");
|
||||
}
|
||||
if (workspace.semantic_index?.vector_store?.collection !== workspace.workspace?.id) {
|
||||
throw new Error("fixture workspace must dedicate one qdrant collection per workspace id");
|
||||
}
|
||||
if (workspace.semantic_index?.vector_store?.dimensions !== 1024) {
|
||||
throw new Error("fixture workspace qdrant dimension changed");
|
||||
}
|
||||
if (workspace.semantic_index?.vector_store?.distance !== "cosine") {
|
||||
throw new Error("fixture workspace qdrant distance changed");
|
||||
}
|
||||
if (workspace.semantic_index?.embedding?.provider !== "ollama_internal") {
|
||||
throw new Error("fixture workspace must use internal ollama embeddings");
|
||||
}
|
||||
if (workspace.semantic_index?.embedding?.model !== "qwen3-embedding:0.6b") {
|
||||
throw new Error("fixture workspace embedding model changed");
|
||||
}
|
||||
if (workspace.semantic_index?.embedding?.dimensions !== 1024) {
|
||||
throw new Error("fixture workspace embedding dimension changed");
|
||||
if (workspace.workspace?.schema_version !== 4) throw new Error("fixture workspace must use schema v4");
|
||||
for (const forbidden of ["semantic_index", "llm_policy"]) {
|
||||
if (Object.hasOwn(workspace, forbidden)) {
|
||||
throw new Error(`fixture workspace must not own installation model configuration: ${forbidden}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!statSync(bundleSource).isFile()) {
|
||||
@@ -139,28 +124,36 @@ if (profile === "local") {
|
||||
throw new Error("server runtime fixture lacks one readable, read-only password-file bind");
|
||||
}
|
||||
accessSync(runtimePasswordSourceInput, constants.R_OK);
|
||||
const piTargets = [
|
||||
"/home/thoth/.pi/agent/auth.json",
|
||||
const generatedPiTargets = [
|
||||
"/home/thoth/.pi/agent/models.json",
|
||||
"/home/thoth/.pi/agent/settings.json",
|
||||
] as const;
|
||||
const piParent = mounts.filter((mount: any) => mount.target === "/home/thoth/.pi");
|
||||
if (piParent.length !== 1) throw new Error("core lacks exactly one Pi state mount");
|
||||
for (const target of piTargets) {
|
||||
for (const target of generatedPiTargets) {
|
||||
const selected = mounts.filter((mount: any) => mount.target === target);
|
||||
if (selected.length !== 1 || selected[0].type !== "bind" || !selected[0].read_only) {
|
||||
throw new Error(`Pi fixture mount is not one generated read-only bind: ${target}`);
|
||||
}
|
||||
accessSync(selected[0].source, constants.R_OK);
|
||||
if (profile === "local") {
|
||||
if (piParent[0].type !== "volume" || selected.length !== 0) {
|
||||
throw new Error(`local Pi fixture must come only from the projected state volume: ${target}`);
|
||||
if (piParent[0].type !== "volume") {
|
||||
throw new Error(`local Pi parent is not a state volume: ${target}`);
|
||||
}
|
||||
} else {
|
||||
if (selected.length !== 1 || selected[0].type !== "bind" || !selected[0].read_only) {
|
||||
throw new Error(`Pi fixture mount is not one read-only bind: ${target}`);
|
||||
}
|
||||
accessSync(selected[0].source, constants.R_OK);
|
||||
const hidden = join(piParent[0].source, "agent", basename(target));
|
||||
if (!statSync(hidden).isFile()) throw new Error(`server parent root lacks ${hidden}`);
|
||||
}
|
||||
}
|
||||
const authTarget = "/home/thoth/.pi/agent/auth.json";
|
||||
const authMounts = mounts.filter((mount: any) => mount.target === authTarget);
|
||||
if (profile === "local") {
|
||||
if (authMounts.length !== 0) {
|
||||
throw new Error("local Pi auth must be projected into the state volume, not directly mounted");
|
||||
}
|
||||
} else if (authMounts.length !== 1 || authMounts[0].type !== "bind" || !authMounts[0].read_only) {
|
||||
throw new Error("server Pi auth is not one independent read-only bind");
|
||||
}
|
||||
|
||||
for (const [target, localVolume] of [
|
||||
["/run/thothii-auth", "auth-runtime"],
|
||||
|
||||
@@ -1,121 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fresh Compose flow: mounted Pi policy/auth must produce a selectable, credential-ready provider.
|
||||
# Base Compose is deliberately model-free; `tht start` appends the generated catalog projection.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
tmp_parent="${TMPDIR:-/tmp}"
|
||||
tmp="$(mktemp -d "${tmp_parent%/}/thoth-provider-readiness.XXXXXX")"
|
||||
project="thothii-provider-readiness-$$"
|
||||
compose=(
|
||||
docker compose --project-name "$project" --env-file "$tmp/local.env"
|
||||
-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml"
|
||||
)
|
||||
cleanup() {
|
||||
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' '{}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
|
||||
auth_config="$tmp/auth"
|
||||
mkdir "$auth_config"
|
||||
chmod 0700 "$auth_config"
|
||||
printf '%s\n' \
|
||||
'version: 1' \
|
||||
'mode: local' \
|
||||
'publicUrl: http://127.0.0.1:8080' \
|
||||
'local:' \
|
||||
' usersFile: users.yaml' \
|
||||
>"$auth_config/auth.yaml"
|
||||
node - "$auth_config/users.yaml" <<'NODE'
|
||||
const { argon2 } = require("node:crypto");
|
||||
const { writeFileSync } = require("node:fs");
|
||||
printf '%s\n' 'schemaVersion: 2' >"$tmp/thothii-installation.yaml"
|
||||
printf '%s' 'fixture-catalog-runtime-password' >"$tmp/catalog-runtime-password"
|
||||
printf '%s' 'fixture-catalog-migrator-password' >"$tmp/catalog-migrator-password"
|
||||
mkdir "$tmp/auth"
|
||||
printf '%s\n' 'mode: local' >"$tmp/auth/auth.yaml"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets" "$tmp/thothii-installation.yaml" \
|
||||
"$tmp/catalog-runtime-password" "$tmp/catalog-migrator-password" "$tmp/auth/auth.yaml"
|
||||
chmod 0700 "$tmp/auth"
|
||||
|
||||
const message = Buffer.from("fixture-local-password", "utf8");
|
||||
const nonce = Buffer.from([...Array(16).keys()]);
|
||||
argon2("argon2id", {
|
||||
message,
|
||||
nonce,
|
||||
memory: 65_536,
|
||||
parallelism: 1,
|
||||
tagLength: 32,
|
||||
passes: 3,
|
||||
}, (error, digest) => {
|
||||
message.fill(0);
|
||||
nonce.fill(0);
|
||||
if (error || !digest) throw error ?? new Error("fixture password hash failed");
|
||||
const salt = Buffer.from([...Array(16).keys()]).toString("base64").replaceAll("=", "");
|
||||
const hash = digest.toString("base64").replaceAll("=", "");
|
||||
writeFileSync(process.argv[2], [
|
||||
"version: 1",
|
||||
"users:",
|
||||
" - id: 00000000-0000-4000-8000-000000000001",
|
||||
" username: fixture-user",
|
||||
" displayName: Fixture user",
|
||||
` passwordHash: $argon2id$v=19$m=65536,t=3,p=1$${salt}$${hash}`,
|
||||
" roles:",
|
||||
" - user",
|
||||
" enabled: true",
|
||||
" authRevision: 1",
|
||||
"",
|
||||
].join("\\n"), { mode: 0o600 });
|
||||
});
|
||||
NODE
|
||||
chmod 0600 "$auth_config/auth.yaml" "$auth_config/users.yaml"
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$tmp/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$tmp/thothii.secrets" \
|
||||
"THT_AUTH_CONFIG_ROOT=$auth_config" \
|
||||
'THOTH_CORE_HTTP_PORT=0' \
|
||||
'THOTH_HTTP_PORT=0' \
|
||||
>"$tmp/local.env"
|
||||
rendered="$tmp/rendered.json"
|
||||
THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$tmp/pi-auth.json" \
|
||||
THT_SECRETS_FILE="$tmp/thothii.secrets" \
|
||||
THT_INSTALLATION_CONFIG_SOURCE="$tmp/thothii-installation.yaml" \
|
||||
THT_CATALOG_RUNTIME_PASSWORD_SOURCE="$tmp/catalog-runtime-password" \
|
||||
THT_CATALOG_MIGRATOR_PASSWORD_SOURCE="$tmp/catalog-migrator-password" \
|
||||
THT_AUTH_CONFIG_ROOT="$tmp/auth" \
|
||||
docker compose --project-directory "$root" \
|
||||
-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml" \
|
||||
config --format json >"$rendered"
|
||||
|
||||
"${compose[@]}" up --detach --wait --wait-timeout 90 --build core
|
||||
core_id="$("${compose[@]}" ps -q core)"
|
||||
core_address="$("${compose[@]}" port core 8787 | head -n 1)"
|
||||
|
||||
"${compose[@]}" exec -T core sh -ceu '
|
||||
test -r /home/thoth/.pi/agent/auth.json
|
||||
test -r /home/thoth/.pi/agent/models.json
|
||||
test -r /home/thoth/.pi/agent/settings.json
|
||||
test -r /run/secrets/thothii.secrets
|
||||
'
|
||||
|
||||
curl --fail --silent --show-error "http://$core_address/models" >"$tmp/models.json"
|
||||
node - "$tmp/models.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (!body.models?.some((model) => model.provider === "zai" && model.id === "glm-5.2")) {
|
||||
throw new Error("fresh Compose did not expose the mounted Pi-enabled model");
|
||||
node - "$rendered" <<'NODE'
|
||||
const { readFileSync } = require("node:fs");
|
||||
const config = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const core = config.services?.core;
|
||||
if (!core) throw new Error("base Compose lacks core");
|
||||
for (const name of [
|
||||
"THT_MODEL_CATALOG_FILE",
|
||||
"THT_MODEL_CATALOG_REVISION",
|
||||
"THT_DEFAULT_SESSION_MODEL",
|
||||
"THT_INTERNAL_EMBEDDING_ID",
|
||||
"THT_INTERNAL_EMBEDDING_MODEL",
|
||||
"THT_INTERNAL_EMBEDDING_DIMENSIONS",
|
||||
]) {
|
||||
if (Object.hasOwn(core.environment || {}, name)) {
|
||||
throw new Error(`base Compose invented installation-owned model input ${name}`);
|
||||
}
|
||||
}
|
||||
for (const target of (core.volumes || []).map((mount) => mount.target)) {
|
||||
if (target === "/run/thothii-model-catalog/catalog.json"
|
||||
|| target === "/home/thoth/.pi/agent/models.json"
|
||||
|| target === "/home/thoth/.pi/agent/settings.json") {
|
||||
throw new Error(`base Compose mounted a generated model adapter: ${target}`);
|
||||
}
|
||||
}
|
||||
const installation = (core.configs || []).filter(
|
||||
(entry) => entry.target === "/run/thothii-installation/thothii-installation.yaml",
|
||||
);
|
||||
if (installation.length !== 1 || installation[0].source !== "thothii_installation_config") {
|
||||
throw new Error("base Compose lacks the protected installation descriptor mount");
|
||||
}
|
||||
NODE
|
||||
|
||||
curl --fail --silent --show-error -X PUT \
|
||||
-H 'content-type: application/json' \
|
||||
--data '{"provider":"zai","model":"glm-5.2","reasoning":"low"}' \
|
||||
"http://$core_address/pi-management/config" >"$tmp/configured.json"
|
||||
curl --fail --silent --show-error \
|
||||
"http://$core_address/pi-management/status" >"$tmp/status.json"
|
||||
node - "$tmp/status.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (!body.ready || body.credentials !== "present") {
|
||||
throw new Error("mounted Pi provider is not credential-ready");
|
||||
}
|
||||
if (body.config?.provider !== "zai" || body.config?.model !== "glm-5.2") {
|
||||
throw new Error("Pi provider configuration was not persisted");
|
||||
}
|
||||
NODE
|
||||
if grep -Fq 'fixture-model-api-key' "$rendered"; then
|
||||
echo "rendered base Compose leaked the model key" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$root/deploy/pi/models.json" || -e "$root/deploy/pi/settings.json" ]]; then
|
||||
echo "legacy authored Pi model sources still exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
inspect="$(docker inspect "$core_id")"
|
||||
for secret in fixture-native-auth-key fixture-model-api-key; do
|
||||
if grep -Fq "$secret" <<<"$inspect"; then
|
||||
echo "container inspection leaked $secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Compose provider-readiness contract passed."
|
||||
echo "Base Compose model fail-closed contract passed."
|
||||
|
||||
@@ -13,9 +13,17 @@ printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tm
|
||||
chmod 0600 "$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/thothii.secrets"
|
||||
printf '%s\n' 'schemaVersion: 2' >"$tmp/thothii-installation.yaml"
|
||||
chmod 0600 "$tmp/thothii-installation.yaml"
|
||||
printf '%s' 'fixture-catalog-runtime-password' >"$tmp/catalog-runtime-password"
|
||||
printf '%s' 'fixture-catalog-migrator-password' >"$tmp/catalog-migrator-password"
|
||||
chmod 0600 "$tmp/catalog-runtime-password" "$tmp/catalog-migrator-password"
|
||||
|
||||
export PI_AUTH_FILE="$tmp/pi-auth.json"
|
||||
export THT_SECRETS_FILE="$tmp/thothii.secrets"
|
||||
export THT_INSTALLATION_CONFIG_SOURCE="$tmp/thothii-installation.yaml"
|
||||
export THT_CATALOG_RUNTIME_PASSWORD_SOURCE="$tmp/catalog-runtime-password"
|
||||
export THT_CATALOG_MIGRATOR_PASSWORD_SOURCE="$tmp/catalog-migrator-password"
|
||||
export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces.git"
|
||||
# Let Docker assign loopback ports so this isolated contract test never collides with an operator stack.
|
||||
export THOTH_CORE_HTTP_PORT=0
|
||||
@@ -66,8 +74,6 @@ docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local
|
||||
command -v pi >/dev/null
|
||||
test ! -e /var/run/docker.sock
|
||||
test -r /home/thoth/.pi/agent/auth.json
|
||||
test -r /home/thoth/.pi/agent/models.json
|
||||
test -r /home/thoth/.pi/agent/settings.json
|
||||
test -r /run/secrets/thothii.secrets
|
||||
touch /data/.task5-writable
|
||||
rm /data/.task5-writable
|
||||
|
||||
@@ -86,8 +86,6 @@ for (const [key, value] of Object.entries({
|
||||
THT_AUTH_STATE_ROOT: "/data/auth",
|
||||
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",
|
||||
THT_CATALOG_DB_HOST: "catalog-db",
|
||||
THT_CATALOG_DB_NAME: "thothii_catalog",
|
||||
THT_CATALOG_RUNTIME_USER: "thothii_catalog_runtime",
|
||||
@@ -95,6 +93,9 @@ for (const [key, value] of Object.entries({
|
||||
})) {
|
||||
if (env[key] !== value) throw new Error(`unexpected core ${key}: ${env[key]}`);
|
||||
}
|
||||
for (const key of ["THT_INTERNAL_EMBEDDING_ID", "THT_INTERNAL_EMBEDDING_MODEL", "THT_INTERNAL_EMBEDDING_DIMENSIONS"]) {
|
||||
if (Object.hasOwn(env, key)) throw new Error(`${key} must come only from the generated installation projection`);
|
||||
}
|
||||
const authConfigMounts = (core.volumes || []).filter((mount) => mount.target === "/run/thothii-auth");
|
||||
if (authConfigMounts.length !== 1 || authConfigMounts[0].type !== "bind" || !authConfigMounts[0].read_only) {
|
||||
throw new Error("core must receive exactly one read-only authentication configuration bind");
|
||||
|
||||
@@ -16,20 +16,22 @@ chmod 0600 "$auth_config/auth.yaml"
|
||||
secrets_file="$tmp/thothii.secrets"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$secrets_file"
|
||||
chmod 0600 "$secrets_file"
|
||||
installation_file="$tmp/thothii-installation.yaml"
|
||||
printf '%s\n' 'schemaVersion: 2' >"$installation_file"
|
||||
chmod 0600 "$installation_file"
|
||||
|
||||
rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" \
|
||||
THT_AUTH_CONFIG_ROOT="$auth_config" docker compose config)
|
||||
THT_AUTH_CONFIG_ROOT="$auth_config" THT_INSTALLATION_CONFIG_SOURCE="$installation_file" \
|
||||
docker compose config)
|
||||
printf '%s\n' "$rendered" | grep -q "source: $auth_file"
|
||||
printf '%s\n' "$rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
|
||||
printf '%s\n' "$rendered" | grep -A4 'target: /home/thoth/.pi/agent/auth.json' \
|
||||
| grep -q 'read_only: true'
|
||||
for target in \
|
||||
/home/thoth/.pi/agent/models.json \
|
||||
/home/thoth/.pi/agent/settings.json; do
|
||||
printf '%s\n' "$rendered" | grep -q "target: $target"
|
||||
printf '%s\n' "$rendered" | grep -A4 "target: $target" | grep -q 'read_only: true'
|
||||
done
|
||||
if printf '%s\n' "$rendered" | grep -Eq '/home/thoth/\.pi/agent/(models|settings)\.json'; then
|
||||
echo "base Compose must not mount model projections without the generated override" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$rendered" | grep -q "file: $secrets_file"
|
||||
printf '%s\n' "$rendered" | grep -q 'target: thothii.secrets'
|
||||
if grep -Fq 'fixture-model-api-key' <<<"$rendered"; then
|
||||
@@ -39,12 +41,14 @@ fi
|
||||
|
||||
dev_rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" \
|
||||
THT_AUTH_CONFIG_ROOT="$auth_config" \
|
||||
THT_AUTH_CONFIG_ROOT="$auth_config" THT_INSTALLATION_CONFIG_SOURCE="$installation_file" \
|
||||
docker compose --env-file deploy/env/local.env.example -f docker-compose.dev.yml config)
|
||||
printf '%s\n' "$dev_rendered" | grep -q "source: $auth_file"
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/models.json'
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/settings.json'
|
||||
if printf '%s\n' "$dev_rendered" | grep -Eq '/home/thoth/\.pi/agent/(models|settings)\.json'; then
|
||||
echo "development Compose must not contain legacy model sources" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$dev_rendered" | grep -q "file: $secrets_file"
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: thothii.secrets'
|
||||
if grep -Fq 'fixture-model-api-key' <<<"$dev_rendered"; then
|
||||
@@ -52,27 +56,8 @@ if grep -Fq 'fixture-model-api-key' <<<"$dev_rendered"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
settings = json.loads(Path("deploy/pi/settings.json").read_text())
|
||||
assert settings["enabledModels"] == [
|
||||
"zai/glm-5.3",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"local-qwen/qwen3.6-35b-a3b",
|
||||
]
|
||||
models = json.loads(Path("deploy/pi/models.json").read_text())
|
||||
qwen = models["providers"]["local-qwen"]
|
||||
assert qwen["api"] == "openai-completions"
|
||||
assert qwen["models"][0]["id"] == "qwen3.6-35b-a3b"
|
||||
assert qwen["models"][0]["compat"]["maxTokensField"] == "max_tokens"
|
||||
assert "aritmolab" not in models["providers"]
|
||||
PY
|
||||
|
||||
if grep -R -n 'registerProvider' harness/.pi/extensions; then
|
||||
echo "Pi model providers must be declared in deploy/pi/models.json, not in code" >&2
|
||||
echo "Pi model providers must be declared in the Installation Model Catalog, not in code" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ TASK13_AUTHENTIK_API_TOKEN="fixture-authentik-token-$profile"
|
||||
TASK13_FRONTEND_PORT=18080
|
||||
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/runtime-password"
|
||||
TASK13_SESSION_CA="$fixture/session-ca.pem"
|
||||
TASK13_PI_MODELS="$fixture/models.json"
|
||||
TASK13_PI_SETTINGS="$fixture/settings.json"
|
||||
TASK13_LLM_SERVER="$fixture/fake-llm.mjs"
|
||||
TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm"
|
||||
TASK13_REMOTE="$fixture/remote.git"
|
||||
@@ -54,22 +52,8 @@ cp "$root/scripts/fixtures/workspace-registry-task13.yaml" "$workspace"
|
||||
|
||||
if [[ "$profile" == local ]]; then
|
||||
task13_write_fixture_files
|
||||
node - "$TASK13_PI_MODELS" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const models = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const model = models.providers?.["local-qwen"]?.models?.find(
|
||||
(candidate) => candidate?.id === "task13-smoke",
|
||||
);
|
||||
if (!model || JSON.stringify(model.input) !== JSON.stringify(["text"])) {
|
||||
throw new Error("Task 13 provider smoke model must declare text input support");
|
||||
}
|
||||
const expectedCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
if (JSON.stringify(model.cost) !== JSON.stringify(expectedCost)) {
|
||||
throw new Error("Task 13 provider smoke model must declare complete zero-cost metadata");
|
||||
}
|
||||
NODE
|
||||
task13_write_environment /fixtures/remote.git
|
||||
compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml" -f "$TASK13_OVERRIDE")
|
||||
compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml" -f "$TASK13_OVERRIDE" -f "$fixture/generated/compose.models.yaml")
|
||||
else
|
||||
TASK13_SERVER_DATA="$fixture/Server Data"
|
||||
TASK13_SERVER_PI_STATE="$fixture/Server Pi State"
|
||||
@@ -106,6 +90,7 @@ else
|
||||
-f "$root/deploy/compose.server.yaml"
|
||||
-f "$root/deploy/compose.session-server.yaml.example"
|
||||
-f "$TASK13_OVERRIDE"
|
||||
-f "$fixture/generated/compose.models.yaml"
|
||||
-f "$root/deploy/compose.auth-runtime-projection.yaml"
|
||||
)
|
||||
fi
|
||||
@@ -188,7 +173,7 @@ NODE
|
||||
fi
|
||||
done
|
||||
|
||||
for mutation in collection-reuse dimension-change; do
|
||||
for mutation in workspace-semantic-index workspace-llm-policy; do
|
||||
mutated="$fixture/$mutation.yaml"
|
||||
node - "$root/backend/package.json" "$workspace" "$mutated" "$mutation" <<'NODE'
|
||||
const fs = require("fs");
|
||||
@@ -197,11 +182,12 @@ const requireFromBackend = createRequire(process.argv[2]);
|
||||
const yaml = requireFromBackend("yaml");
|
||||
const [source, destination, mutation] = process.argv.slice(3);
|
||||
const workspace = yaml.parse(fs.readFileSync(source, "utf8"));
|
||||
if (mutation === "collection-reuse") {
|
||||
workspace.semantic_index.vector_store.collection = "shared-semantic";
|
||||
} else if (mutation === "dimension-change") {
|
||||
workspace.semantic_index.vector_store.dimensions = 1536;
|
||||
workspace.semantic_index.embedding.dimensions = 1536;
|
||||
if (mutation === "workspace-semantic-index") {
|
||||
workspace.semantic_index = {
|
||||
vector_store: { engine: "qdrant", collection: "shared-semantic", dimensions: 1536 },
|
||||
};
|
||||
} else if (mutation === "workspace-llm-policy") {
|
||||
workspace.llm_policy = { provider: "openai", model: "gpt-test" };
|
||||
}
|
||||
fs.writeFileSync(destination, yaml.stringify(workspace));
|
||||
NODE
|
||||
|
||||
@@ -48,11 +48,12 @@ if (!Object.hasOwn(coreEnv, "THT_LLM_URL")) {
|
||||
for (const [key, value] of Object.entries({
|
||||
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",
|
||||
})) {
|
||||
if (coreEnv[key] !== value) throw new Error(`unexpected core ${key}: ${coreEnv[key]}`);
|
||||
}
|
||||
for (const key of ["THT_INTERNAL_EMBEDDING_ID", "THT_INTERNAL_EMBEDDING_MODEL", "THT_INTERNAL_EMBEDDING_DIMENSIONS"]) {
|
||||
if (Object.hasOwn(coreEnv, key)) throw new Error(`${key} must come only from the generated installation projection`);
|
||||
}
|
||||
for (const forbidden of ["THT_VEC_REST_URL", "THT_VEC_WRITE_REST_URL", "THT_OLLAMA_URL"]) {
|
||||
if (Object.hasOwn(coreEnv, forbidden) && coreEnv[forbidden] !== "") {
|
||||
throw new Error(`core must not require external semantic binding ${forbidden}`);
|
||||
@@ -189,13 +190,14 @@ if (!Object.hasOwn(config.services.core.environment || {}, "THT_LLM_URL")) {
|
||||
for (const [key, value] of Object.entries({
|
||||
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",
|
||||
})) {
|
||||
if (config.services.core.environment?.[key] !== value) {
|
||||
throw new Error(`unexpected core ${key}: ${config.services.core.environment?.[key]}`);
|
||||
}
|
||||
}
|
||||
for (const key of ["THT_INTERNAL_EMBEDDING_ID", "THT_INTERNAL_EMBEDDING_MODEL", "THT_INTERNAL_EMBEDDING_DIMENSIONS"]) {
|
||||
if (Object.hasOwn(config.services.core.environment || {}, key)) throw new Error(`${key} must come only from the generated installation projection`);
|
||||
}
|
||||
for (const serviceName of ["qdrant", "embedding", "embedding-model-init"]) {
|
||||
if ((config.services[serviceName].ports || []).length !== 0) {
|
||||
throw new Error(`${serviceName} must not publish a host port`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression tests for the fail-closed schema-v3-only absence gate.
|
||||
# Regression tests for the fail-closed schema-v4-only absence gate.
|
||||
set -euo pipefail
|
||||
|
||||
project_root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -25,7 +25,7 @@ fail() {
|
||||
}
|
||||
|
||||
write_fixture_descriptor() {
|
||||
local path="$1" workspace_key="${2:-workspace:}" schema_key="${3:- schema_version: 3}"
|
||||
local path="$1" workspace_key="${2:-workspace:}" schema_key="${3:- schema_version: 4}"
|
||||
printf '%s\n' "$workspace_key" "$schema_key" >"$path"
|
||||
cat >>"$path" <<'YAML'
|
||||
id: fixture-workspace
|
||||
@@ -36,18 +36,6 @@ dwh:
|
||||
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
|
||||
}
|
||||
|
||||
@@ -62,7 +50,7 @@ seed_fixture() {
|
||||
"$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 schemaVersion = 4;' >"$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"
|
||||
@@ -74,7 +62,7 @@ break.ts"
|
||||
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"
|
||||
printf '%s\n' 'Write-Output "schema v4"' >"$fixture/scripts/test-windows-clone-contract.ps1"
|
||||
"$real_git" -C "$fixture" add .
|
||||
"$real_git" -C "$fixture" commit -qm seed
|
||||
}
|
||||
@@ -307,14 +295,14 @@ 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.
|
||||
# Quoted/space/indented YAML keys are real mappings; v4 passes and non-v4 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"
|
||||
" 'schema_version' : 4"
|
||||
commit_fixture quoted-yaml-v4
|
||||
expect_pass "quoted and indented workspace v4"
|
||||
|
||||
seed_fixture
|
||||
cat >"$fixture/deploy/workspaces/example.yaml" <<'EOF'
|
||||
@@ -325,7 +313,7 @@ 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"
|
||||
printf '%s\n' 'workspace: { schema_version: 4 }' >"$fixture/deploy/workspaces/example.yaml"
|
||||
commit_fixture inline-workspace
|
||||
expect_rejected "inline workspace mapping" "deploy/workspaces/example.yaml"
|
||||
|
||||
@@ -388,8 +376,8 @@ printf '%s\n' 'const revision = { revision: { id: "x", state: "operational" } };
|
||||
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
|
||||
# A top-level workspace descriptor has one exact schema_version: 4 key.
|
||||
for malformed in schema-v1 schema-v2 schema-v3 leading-zero hexadecimal multiline duplicate; do
|
||||
seed_fixture
|
||||
case "$malformed" in
|
||||
schema-v1)
|
||||
@@ -398,17 +386,20 @@ for malformed in schema-v1 schema-v2 leading-zero hexadecimal multiline duplicat
|
||||
schema-v2)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
schema-v3)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 3' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
leading-zero)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 02' >"$fixture/deploy/workspaces/example.yaml"
|
||||
printf '%s\n' 'workspace:' ' schema_version: 04' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
hexadecimal)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 0x2' >"$fixture/deploy/workspaces/example.yaml"
|
||||
printf '%s\n' 'workspace:' ' schema_version: 0x4' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
multiline)
|
||||
printf '%s\n' 'workspace:' ' schema_version: >' ' 3' >"$fixture/deploy/workspaces/example.yaml"
|
||||
printf '%s\n' 'workspace:' ' schema_version: >' ' 4' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
duplicate)
|
||||
printf '%s\n' 'workspace:' ' schema_version: 3' ' schema_version: 3' >"$fixture/deploy/workspaces/example.yaml"
|
||||
printf '%s\n' 'workspace:' ' schema_version: 4' ' schema_version: 4' >"$fixture/deploy/workspaces/example.yaml"
|
||||
;;
|
||||
esac
|
||||
commit_fixture "yaml-$malformed"
|
||||
|
||||
@@ -91,8 +91,8 @@ CASES = [
|
||||
|
||||
def workspace_region(extra=""):
|
||||
body = (
|
||||
"Schema v3 is the only accepted workspace descriptor. "
|
||||
"Schema v1 and v2 workspace descriptors are rejected before activation."
|
||||
"Schema v4 is the only accepted workspace descriptor. "
|
||||
"Schema v1, v2, and v3 workspace descriptors are rejected before activation."
|
||||
)
|
||||
if extra:
|
||||
body += "\n" + extra
|
||||
@@ -335,16 +335,16 @@ class RegionStructureTests(unittest.TestCase):
|
||||
def test_noncanonical_contract_sentences_are_rejected(self):
|
||||
variants = (
|
||||
workspace_region().replace(
|
||||
"Schema v3 is the only accepted workspace descriptor.",
|
||||
"Only schema v3 workspace descriptors are accepted.",
|
||||
"Schema v4 is the only accepted workspace descriptor.",
|
||||
"Only schema v4 workspace descriptors are accepted.",
|
||||
),
|
||||
workspace_region().replace(
|
||||
"Schema v3 is the only accepted workspace descriptor.",
|
||||
"Not Schema v3 is the only accepted workspace descriptor.",
|
||||
"Schema v4 is the only accepted workspace descriptor.",
|
||||
"Not Schema v4 is the only accepted workspace descriptor.",
|
||||
),
|
||||
workspace_region().replace(
|
||||
"Schema v1 and v2 workspace descriptors are rejected before activation.",
|
||||
"Not Schema v1 and v2 workspace descriptors are rejected before activation.",
|
||||
"Schema v1, v2, and v3 workspace descriptors are rejected before activation.",
|
||||
"Not Schema v1, v2, and v3 workspace descriptors are rejected before activation.",
|
||||
),
|
||||
)
|
||||
for index, noncanonical in enumerate(variants):
|
||||
|
||||
@@ -317,12 +317,14 @@ task13_compose_files() {
|
||||
-f "$TASK13_ROOT/deploy/compose.server.yaml"
|
||||
-f "$TASK13_ROOT/deploy/compose.session-server.yaml.example"
|
||||
-f "$TASK13_OVERRIDE"
|
||||
-f "${TASK13_INSTALLATION%/*}/generated/compose.models.yaml"
|
||||
-f "$TASK13_ROOT/deploy/compose.auth-runtime-projection.yaml"
|
||||
)
|
||||
else
|
||||
TASK13_COMPOSE+=(
|
||||
-f "$TASK13_ROOT/deploy/compose.local.yaml"
|
||||
-f "$TASK13_OVERRIDE"
|
||||
-f "${TASK13_INSTALLATION%/*}/generated/compose.models.yaml"
|
||||
)
|
||||
fi
|
||||
if [[ "${TASK13_PROFILE:-local}" == local && -f "$TASK13_CURRENT_IMAGE_OVERRIDE" ]]; then
|
||||
@@ -414,6 +416,100 @@ EOF
|
||||
chmod 0600 "$TASK13_SESSION_CA"
|
||||
}
|
||||
|
||||
# Test-only materialization of the same deterministic boundary adapters covered by the Go
|
||||
# modelprojection tests. Production lifecycle commands always generate these files themselves.
|
||||
task13_write_model_projection_fixture() {
|
||||
python3 - "$TASK13_INSTALLATION" "$TASK13_LLM_CONTAINER" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
installation = Path(sys.argv[1])
|
||||
provider_host = sys.argv[2]
|
||||
generated = installation.parent / "generated"
|
||||
(generated / "pi").mkdir(parents=True, exist_ok=True)
|
||||
catalog = {
|
||||
"schemaVersion": 1,
|
||||
"defaultSession": "local-qwen/task13-smoke",
|
||||
"embedding": {"id": "ollama/qwen3-embedding:0.6b", "dimensions": 1024},
|
||||
"models": [{
|
||||
"id": "local-qwen/task13-smoke",
|
||||
"provider": "local-qwen",
|
||||
"model": "task13-smoke",
|
||||
"label": "Task 13 deterministic smoke",
|
||||
"upstreamModel": "task13-smoke",
|
||||
"endpoint": {"baseUrl": f"http://{provider_host}:9000/v1"},
|
||||
"authentication": {"mode": "none"},
|
||||
"sessionAdapter": {"mode": "openai_compatible"},
|
||||
"session": {
|
||||
"reasoning": False,
|
||||
"input": ["text"],
|
||||
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
|
||||
"contextWindow": 4096,
|
||||
"maxTokens": 64,
|
||||
},
|
||||
}],
|
||||
}
|
||||
models = {
|
||||
"providers": {"local-qwen": {
|
||||
"baseUrl": f"http://{provider_host}:9000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "local",
|
||||
"models": [{
|
||||
"id": "task13-smoke", "name": "Task 13 deterministic smoke",
|
||||
"reasoning": False, "input": ["text"],
|
||||
"cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
|
||||
"contextWindow": 4096, "maxTokens": 64,
|
||||
}],
|
||||
}},
|
||||
}
|
||||
settings = {"defaultProjectTrust": "always", "enabledModels": ["local-qwen/task13-smoke"]}
|
||||
serialized = []
|
||||
for path, value in (
|
||||
(generated / "catalog.json", catalog),
|
||||
(generated / "pi/models.json", models),
|
||||
(generated / "pi/settings.json", settings),
|
||||
):
|
||||
raw = json.dumps(value, indent=2, sort_keys=True) + "\n"
|
||||
path.write_text(raw)
|
||||
serialized.append(raw.encode())
|
||||
revision = "sha256:" + hashlib.sha256(b"".join(serialized)).hexdigest()
|
||||
quote = json.dumps
|
||||
(generated / "compose.models.yaml").write_text(f"""services:
|
||||
core:
|
||||
environment:
|
||||
THT_MODEL_CATALOG_FILE: /run/thothii-model-catalog/catalog.json
|
||||
THT_MODEL_CATALOG_REVISION: {quote(revision)}
|
||||
THT_DEFAULT_SESSION_MODEL: local-qwen/task13-smoke
|
||||
THT_INTERNAL_EMBEDDING_ID: ollama/qwen3-embedding:0.6b
|
||||
THT_INTERNAL_EMBEDDING_MODEL: qwen3-embedding:0.6b
|
||||
THT_INTERNAL_EMBEDDING_DIMENSIONS: '1024'
|
||||
volumes:
|
||||
- type: bind
|
||||
source: {quote(str(generated / 'catalog.json'))}
|
||||
target: /run/thothii-model-catalog/catalog.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: {quote(str(generated / 'pi/models.json'))}
|
||||
target: /home/thoth/.pi/agent/models.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: {quote(str(generated / 'pi/settings.json'))}
|
||||
target: /home/thoth/.pi/agent/settings.json
|
||||
read_only: true
|
||||
workspace-maintenance:
|
||||
environment:
|
||||
THT_INTERNAL_EMBEDDING_ID: ollama/qwen3-embedding:0.6b
|
||||
THT_INTERNAL_EMBEDDING_MODEL: qwen3-embedding:0.6b
|
||||
THT_INTERNAL_EMBEDDING_DIMENSIONS: '1024'
|
||||
embedding-model-init:
|
||||
environment:
|
||||
OLLAMA_MODEL: qwen3-embedding:0.6b
|
||||
""")
|
||||
PY
|
||||
}
|
||||
|
||||
task13_write_fixture_files() {
|
||||
printf '{}\n' >"$TASK13_PI_AUTH"
|
||||
printf 'THT_MODEL_API_KEY=%s\n' "$TASK13_SECRET_VALUE" >"$TASK13_SECRETS"
|
||||
@@ -425,41 +521,6 @@ task13_write_fixture_files() {
|
||||
chmod 0700 "$TASK13_AUTH_ROOT"
|
||||
chmod 0600 "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" "$TASK13_AUTH_PASSWORD_FILE"
|
||||
|
||||
cat >"$TASK13_PI_MODELS" <<EOF
|
||||
{
|
||||
"providers": {
|
||||
"local-qwen": {
|
||||
"baseUrl": "http://$TASK13_LLM_CONTAINER:9000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "task13-local-provider",
|
||||
"models": [
|
||||
{
|
||||
"id": "task13-smoke",
|
||||
"name": "Task 13 deterministic smoke",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 4096,
|
||||
"maxTokens": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cat >"$TASK13_PI_SETTINGS" <<'EOF'
|
||||
{
|
||||
"defaultProjectTrust": "always",
|
||||
"enabledModels": ["local-qwen/task13-smoke"]
|
||||
}
|
||||
EOF
|
||||
chmod 0644 "$TASK13_PI_MODELS" "$TASK13_PI_SETTINGS"
|
||||
|
||||
cat >"$TASK13_LLM_SERVER" <<'EOF'
|
||||
import http from "node:http";
|
||||
|
||||
@@ -517,8 +578,6 @@ services:
|
||||
labels:
|
||||
io.thothii.task13.run: "$TASK13_RUN_ID"
|
||||
environment:
|
||||
PI_PROVIDER: local-qwen
|
||||
PI_MODEL: task13-smoke
|
||||
PI_THINKING: low
|
||||
THT_WORKSPACE_INSTALLATION_ID: task13-smoke
|
||||
THT_WORKSPACE_REGISTRY_ROOT: /data/workspace-registry
|
||||
@@ -605,15 +664,40 @@ EOF
|
||||
chmod 0600 "$TASK13_OVERRIDE"
|
||||
|
||||
cat >"$TASK13_INSTALLATION" <<EOF
|
||||
schemaVersion: 2
|
||||
profile: local
|
||||
projectDirectory: "$TASK13_ROOT"
|
||||
envFile: "$TASK13_ENV_FILE"
|
||||
modelCatalog:
|
||||
defaults:
|
||||
session: local-qwen/task13-smoke
|
||||
embedding:
|
||||
id: ollama/qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
providers:
|
||||
local-qwen:
|
||||
endpoint:
|
||||
baseUrl: http://$TASK13_LLM_CONTAINER:9000/v1
|
||||
authentication:
|
||||
mode: none
|
||||
session:
|
||||
mode: openai_compatible
|
||||
models:
|
||||
task13-smoke:
|
||||
label: Task 13 deterministic smoke
|
||||
session:
|
||||
reasoning: false
|
||||
input: [text]
|
||||
cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}
|
||||
contextWindow: 4096
|
||||
maxTokens: 64
|
||||
authentication:
|
||||
configDirectory: "$TASK13_AUTH_ROOT"
|
||||
overrides:
|
||||
- "$TASK13_OVERRIDE"
|
||||
EOF
|
||||
chmod 0600 "$TASK13_INSTALLATION"
|
||||
task13_write_model_projection_fixture
|
||||
}
|
||||
|
||||
task13_write_server_fixture_files() {
|
||||
@@ -805,9 +889,33 @@ EOF
|
||||
chmod 0600 "$TASK13_ENV_FILE"
|
||||
|
||||
cat >"$TASK13_INSTALLATION" <<EOF
|
||||
schemaVersion: 2
|
||||
profile: server
|
||||
projectDirectory: "$TASK13_ROOT"
|
||||
envFile: "$TASK13_ENV_FILE"
|
||||
modelCatalog:
|
||||
defaults:
|
||||
session: local-qwen/task13-smoke
|
||||
embedding:
|
||||
id: ollama/qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
providers:
|
||||
local-qwen:
|
||||
endpoint:
|
||||
baseUrl: http://$TASK13_LLM_CONTAINER:9000/v1
|
||||
authentication:
|
||||
mode: none
|
||||
session:
|
||||
mode: openai_compatible
|
||||
models:
|
||||
task13-smoke:
|
||||
label: Task 13 deterministic smoke
|
||||
session:
|
||||
reasoning: false
|
||||
input: [text]
|
||||
cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0}
|
||||
contextWindow: 4096
|
||||
maxTokens: 64
|
||||
authentication:
|
||||
configDirectory: "$TASK13_AUTH_ROOT"
|
||||
runtimeProjection:
|
||||
@@ -819,6 +927,7 @@ overrides:
|
||||
- "$TASK13_OVERRIDE"
|
||||
EOF
|
||||
chmod 0600 "$TASK13_INSTALLATION"
|
||||
task13_write_model_projection_fixture
|
||||
}
|
||||
|
||||
task13_workspace_metadata() {
|
||||
@@ -1058,23 +1167,17 @@ task13_prepare_local_pi_runtime() {
|
||||
--user 0:0 \
|
||||
--entrypoint sh \
|
||||
--volume "$TASK13_PI_AUTH:/source/auth.json:ro" \
|
||||
--volume "$TASK13_PI_MODELS:/source/models.json:ro" \
|
||||
--volume "$TASK13_PI_SETTINGS:/source/settings.json:ro" \
|
||||
--volume "$TASK13_PI_RUNTIME_VOLUME:/target" \
|
||||
"$TASK13_CORE_IMAGE" -ceu '
|
||||
test -f /source/auth.json && test ! -L /source/auth.json
|
||||
test -f /source/models.json && test ! -L /source/models.json
|
||||
test -f /source/settings.json && test ! -L /source/settings.json
|
||||
test -d /target/agent && test ! -L /target/agent
|
||||
test -z "$(find /target -mindepth 1 -maxdepth 1 ! -name agent -print -quit)"
|
||||
test -z "$(find /target/agent -mindepth 1 -maxdepth 1 -print -quit)"
|
||||
cp /source/auth.json /source/models.json /source/settings.json /target/agent/
|
||||
cp /source/auth.json /target/agent/
|
||||
chown -R 10001:10001 /target
|
||||
chmod 0700 /target /target/agent
|
||||
chmod 0600 /target/agent/auth.json /target/agent/models.json /target/agent/settings.json
|
||||
chmod 0600 /target/agent/auth.json
|
||||
test "$(stat -c "%u:%g:%a" /target/agent/auth.json)" = 10001:10001:600
|
||||
test "$(stat -c "%u:%g:%a" /target/agent/models.json)" = 10001:10001:600
|
||||
test "$(stat -c "%u:%g:%a" /target/agent/settings.json)" = 10001:10001:600
|
||||
'
|
||||
}
|
||||
|
||||
@@ -2918,8 +3021,6 @@ task13_initialize() {
|
||||
TASK13_AUTH_PASSWORD="task13-auth-$TASK13_RUN_ID"
|
||||
TASK13_OIDC_CLIENT_SECRET="task13-oidc-client-$TASK13_RUN_ID"
|
||||
TASK13_AUTHENTIK_API_TOKEN="task13-authentik-token-$TASK13_RUN_ID"
|
||||
TASK13_PI_MODELS="$TASK13_TMP/models.json"
|
||||
TASK13_PI_SETTINGS="$TASK13_TMP/pi-settings.json"
|
||||
TASK13_LLM_SERVER="$TASK13_TMP/fake-llm.mjs"
|
||||
TASK13_OIDC_SERVER="$TASK13_TMP/fake-oidc.mjs"
|
||||
TASK13_OIDC_CERT="$TASK13_TMP/fake-oidc-cert.pem"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reproducible schema-v3-only release gate. The Git checkout is the trust root;
|
||||
# Reproducible schema-v4-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
|
||||
@@ -12,7 +12,7 @@ export NPM_CONFIG_GLOBALCONFIG=<private-empty-global-config>
|
||||
/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)
|
||||
(cd backend && npm run test:schema-v4-verifier)
|
||||
/bin/bash scripts/test-verify-schema-v3-only.sh
|
||||
/bin/bash scripts/verify-schema-v3-only.sh
|
||||
EOF
|
||||
@@ -29,6 +29,6 @@ export NPM_CONFIG_GLOBALCONFIG="$release_tmp/npm-globalconfig"
|
||||
/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)
|
||||
(cd "$root/backend" && npm run test:schema-v4-verifier)
|
||||
/bin/bash "$root/scripts/test-verify-schema-v3-only.sh"
|
||||
/bin/bash "$root/scripts/verify-schema-v3-only.sh"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fail-closed absence gate for the supported schema-v3-only workspace runtime.
|
||||
# Fail-closed absence gate for the supported schema-v4-only workspace runtime.
|
||||
set -euo pipefail
|
||||
shopt -s nocasematch
|
||||
|
||||
@@ -31,7 +31,7 @@ Release trust anchor and order (durable entry point):
|
||||
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.
|
||||
and the full schema-v4-only gate, which repeats trust checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ if ! root="$(cd "$root_argument" 2>/dev/null && pwd -P)"; then
|
||||
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")"
|
||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v4-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
|
||||
@@ -88,6 +88,7 @@ is_allowed_match() {
|
||||
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/src/workspaces/schema.ts) return 0 ;;
|
||||
migration-marker:backend/scripts/clean-dist.test.mjs) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
@@ -204,7 +205,7 @@ bootstrap_files=(
|
||||
)
|
||||
if [[ $bootstrap_trust_only -eq 1 ]]; then
|
||||
require_trusted_files_at "$root" "${bootstrap_files[@]}"
|
||||
echo "schema-v3-only bootstrap trust passed"
|
||||
echo "schema-v4-only bootstrap trust passed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -272,4 +273,4 @@ if [[ $runtime_only -eq 0 ]]; then
|
||||
--document "$root/docs/operations/workspaces.md"
|
||||
fi
|
||||
|
||||
echo "schema-v3-only absence gate passed"
|
||||
echo "schema-v4-only absence gate passed"
|
||||
|
||||
@@ -123,8 +123,9 @@ bindings = pathlib.Path(sys.argv[3]).read_text()
|
||||
if local.get("profile") != "local" or server.get("profile") != "server":
|
||||
raise SystemExit("installation examples must retain their local/server profiles")
|
||||
for document in (local, server):
|
||||
if "metadataGeneration" not in document or "authentication" not in document:
|
||||
raise SystemExit("installation example lacks metadata or authentication configuration")
|
||||
catalog = document.get("modelCatalog")
|
||||
if not isinstance(catalog, dict) or "providers" not in catalog or "authentication" not in document:
|
||||
raise SystemExit("installation example lacks model catalog or authentication configuration")
|
||||
if re.search(r"(?:KEY|PASSWORD|TOKEN|SECRET)=[^\n#<][^\n]*", bindings):
|
||||
raise SystemExit("workspace bindings example contains a secret value")
|
||||
PY
|
||||
|
||||
@@ -14,9 +14,9 @@ from pathlib import Path
|
||||
WORKSPACE_REGION = "workspace-descriptor-contract"
|
||||
NON_WORKSPACE_REGION = "non-workspace-migration"
|
||||
HISTORICAL_ARCHIVE_H1 = "# Historical archive"
|
||||
CANONICAL_V3_SENTENCE = "Schema v3 is the only accepted workspace descriptor."
|
||||
CANONICAL_V4_SENTENCE = "Schema v4 is the only accepted workspace descriptor."
|
||||
CANONICAL_REJECTION_SENTENCE = (
|
||||
"Schema v1 and v2 workspace descriptors are rejected before activation."
|
||||
"Schema v1, v2, and v3 workspace descriptors are rejected before activation."
|
||||
)
|
||||
|
||||
_MARKER_LINE = re.compile(
|
||||
@@ -34,8 +34,8 @@ _DELETED_TOOL = re.compile(
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_REMAINING_LEGACY_CLAIM = re.compile(
|
||||
r"\b(?:schema(?:[- ]v?|\s+version\s*)[12]|"
|
||||
r"schema_version\s*[:=]\s*[12]|version\s*[12]|v[12]|"
|
||||
r"\b(?:schema(?:[- ]v?|\s+version\s*)[123]|"
|
||||
r"schema_version\s*[:=]\s*[123]|version\s*[123]|v[123]|"
|
||||
r"legacy(?:[-\s]+workspace)?[-\s]+descriptors?)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
@@ -48,9 +48,9 @@ _CODE_RESTORE = str.maketrans(
|
||||
)
|
||||
|
||||
_OUTSIDE_LEGACY_REFERENCE = re.compile(
|
||||
r"\b(?:schema(?:[- ]v?|\s+version\s*)[12]|"
|
||||
r"schema_version\s*[:=]\s*[12]|"
|
||||
r"(?:v[12](?:\s*(?:/|and|or)\s*v[12])?)\s+(?:workspace\s+)?descriptors?|"
|
||||
r"\b(?:schema(?:[- ]v?|\s+version\s*)[123]|"
|
||||
r"schema_version\s*[:=]\s*[123]|"
|
||||
r"(?:v[123](?:\s*(?:/|and|or|,)\s*v[123])*)\s+(?:workspace\s+)?descriptors?|"
|
||||
r"legacy(?:[-\s]+workspace)?[-\s]+descriptors?)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
@@ -355,11 +355,11 @@ def check_document_text(text: str, label: str = "document") -> None:
|
||||
workspace_prose = _render_prose_markdown(
|
||||
scan.prose_text[workspace.start : workspace.end]
|
||||
)
|
||||
if _exact_sentence_count(workspace_prose, CANONICAL_V3_SENTENCE) != 1:
|
||||
raise ContractError(f"{label}: workspace contract lacks the canonical v3-only sentence")
|
||||
if _exact_sentence_count(workspace_prose, CANONICAL_V4_SENTENCE) != 1:
|
||||
raise ContractError(f"{label}: workspace contract lacks the canonical v4-only sentence")
|
||||
if _exact_sentence_count(workspace_prose, CANONICAL_REJECTION_SENTENCE) != 1:
|
||||
raise ContractError(
|
||||
f"{label}: workspace contract lacks the canonical v1/v2 rejection sentence"
|
||||
f"{label}: workspace contract lacks the canonical v1/v2/v3 rejection sentence"
|
||||
)
|
||||
|
||||
workspace_visible = _render_markdown(scan.operator_text[workspace.start : workspace.end])
|
||||
|
||||
Reference in New Issue
Block a user