fix: stabilize runtime workspace identity

This commit is contained in:
2026-08-05 16:39:23 +02:00
parent bd798b1c96
commit b93f13e39e
8 changed files with 195 additions and 9 deletions
@@ -129,6 +129,7 @@ export function renderRuntimeConfig(
runtime_identity: {
workspace_id: identity.workspaceId,
workspace_revision: identity.workspaceRevision,
source_identity: `workspace://${identity.workspaceId}`,
},
} : {}),
...(installation.session_storage === undefined
+28 -1
View File
@@ -1,9 +1,12 @@
import { execFile } from "node:child_process";
import { chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { afterEach, expect, test, vi } from "vitest";
import { parse } from "yaml";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { ThtRunner } from "../src/tht/tht-runner.js";
@@ -142,6 +145,30 @@ test("real schema-v2 registry revision loads through ThtRunner and the harness c
expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]);
});
test("separate runtime leases hand off one stable logical workspace identity", async () => {
const f = await fixture();
const runner = runnerFor(f);
const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
const second = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
try {
expect(first.path).not.toBe(second.path);
expect(parse(readFileSync(first.path, "utf8")).runtime_identity).toEqual({
workspace_id: "psd-clinical",
workspace_revision: f.revision.commit,
source_identity: "workspace://psd-clinical",
});
expect(parse(readFileSync(second.path, "utf8")).runtime_identity).toEqual({
workspace_id: "psd-clinical",
workspace_revision: f.revision.commit,
source_identity: "workspace://psd-clinical",
});
} finally {
first.release();
second.release();
}
});
test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => {
const f = await fixture();
const app = buildApp(loadConfig({
@@ -104,10 +104,18 @@ test("runtime support stays fail-closed for either SSH connector", () => {
});
test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
const yaml = renderRuntimeConfig(workspace, directBindings, paths);
const yaml = renderRuntimeConfig(workspace, directBindings, paths, {
workspaceId: "psd-clinical",
workspaceRevision: "a".repeat(40),
});
const rendered = parse(yaml);
expect(rendered).toMatchObject({
runtime_identity: {
workspace_id: "psd-clinical",
workspace_revision: "a".repeat(40),
source_identity: "workspace://psd-clinical",
},
language: "it",
database: {
host: "dwh.internal",
+19
View File
@@ -95,6 +95,7 @@ def test_runtime_handoff_preserves_canonical_identity_and_durable_roots(monkeypa
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
source_identity: workspace://psd-clinical
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
@@ -113,6 +114,7 @@ embeddings: {{base_url: http://embedding.invalid, model: embed, dim: 768}}
assert cfg._workspace_id == "psd-clinical"
assert cfg._workspace_revision == "a" * 40
assert cfg._config_source == "workspace://psd-clinical"
assert cfg.paths.sessions == runtime_root / "sessions"
assert cfg.paths.artifacts == runtime_root / "artifacts"
assert cfg.paths.indexes == runtime_root / "indexes"
@@ -135,6 +137,23 @@ roots: {{sessions: sessions, artifacts: artifacts, indexes: indexes}}
assert workspace_id_for_config(cfg, workspace) == "psd-clinical"
def test_runtime_identity_rejects_a_source_for_another_workspace(tmp_path):
workspace = tmp_path / "runtime-random-uuid.yaml"
workspace.write_text(f"""
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
source_identity: workspace://another-workspace
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
roots: {{sessions: sessions, artifacts: artifacts, indexes: indexes}}
""")
with pytest.raises(ConfigError, match="source_identity"):
load_config(workspace)
def test_load_config_rejects_executable_yaml_tags_without_running_them(tmp_path):
marker = tmp_path / "must-not-exist"
workspace = tmp_path / "workspace.yaml"
+50 -1
View File
@@ -5,8 +5,9 @@ from pathlib import Path
from typer.testing import CliRunner
from tht.cli import app
from tht.config import load_config
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline
from tht.jobs.dwh_pipeline import active_generation_dir, config_dwh_binding
from tht.jobs.dwh_pipeline import active_generation_dir, config_dwh_binding, fingerprint
from tht.jobs.dwh_pipeline import resolve_dwh_snapshot
from tht.jobs.dwh_pipeline import lease_dwh_snapshot
from tht.jobs.locking import _lock_name
@@ -15,6 +16,54 @@ from tht.jobs.locking import _lock_name
FP = "sha256:" + hashlib.sha256(b"test").hexdigest()
def _runtime_config(tmp_path, filename, revision, database="warehouse"):
path = tmp_path / filename
path.write_text(f"""
runtime_identity:
workspace_id: demo
workspace_revision: {revision}
source_identity: workspace://demo
dwh:
type: postgres_direct
connection: {{database: {database}, schema: analytics, user: reader, password: secret}}
roots:
sessions: {tmp_path / 'sessions'}
artifacts: {tmp_path / 'artifacts'}
indexes: {tmp_path / 'indexes'}
""")
return path
def test_runtime_lease_path_and_revision_metadata_do_not_change_dwh_binding(tmp_path, monkeypatch):
monkeypatch.delenv("THT_HOME", raising=False)
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
first = load_config(_runtime_config(tmp_path, "runtime-first.yaml", "a" * 40))
second = load_config(_runtime_config(tmp_path, "runtime-second.yaml", "a" * 40))
metadata_only_revision = load_config(
_runtime_config(tmp_path, "runtime-third.yaml", "b" * 40)
)
expected = config_dwh_binding(first)
assert expected == config_dwh_binding(second)
assert expected == config_dwh_binding(metadata_only_revision)
assert expected["input_fingerprint"] == fingerprint("workspace://demo")
def test_effective_dwh_change_invalidates_runtime_binding(tmp_path, monkeypatch):
monkeypatch.delenv("THT_HOME", raising=False)
monkeypatch.delenv("THT_DATA_ROOT", raising=False)
original = load_config(_runtime_config(tmp_path, "runtime-first.yaml", "a" * 40))
changed = load_config(
_runtime_config(tmp_path, "runtime-second.yaml", "b" * 40, database="warehouse_v2")
)
assert (
config_dwh_binding(original)["config_fingerprint"]
!= config_dwh_binding(changed)["config_fingerprint"]
)
def snapshot_config(tmp_path, workspace_id="demo"):
from types import SimpleNamespace
+18 -1
View File
@@ -165,6 +165,19 @@ class PathsConfig(BaseModel):
class RuntimeIdentityConfig(BaseModel):
workspace_id: str = Field(pattern=r"^[a-z][a-z0-9-]{2,62}$")
workspace_revision: str = Field(pattern=r"^[0-9a-f]{40}$")
source_identity: str | None = Field(
default=None,
pattern=r"^workspace://[a-z][a-z0-9-]{2,62}$",
)
@model_validator(mode="after")
def source_matches_workspace(self):
expected = f"workspace://{self.workspace_id}"
if self.source_identity is None:
self.source_identity = expected
elif self.source_identity != expected:
raise ValueError("source_identity must match workspace_id")
return self
class WorkspaceRoots(PathsConfig):
@@ -430,7 +443,11 @@ def load_config(path: Path) -> Config:
if cfg.runtime_identity is not None
else None
)
cfg._config_source = path.resolve().as_posix()
cfg._config_source = (
cfg.runtime_identity.source_identity
if cfg.runtime_identity is not None
else path.resolve().as_posix()
)
return cfg
+3
View File
@@ -56,6 +56,9 @@ def config_dwh_binding(cfg) -> dict[str, str]:
# Session persistence has no bearing on schema/LSH artifacts. Excluding it keeps an
# opt-in session-storage deployment from invalidating an otherwise identical DWH cache.
payload.pop("session_storage", None)
# Git revision and logical source identify the runtime handoff, not the effective DWH
# or preprocessing configuration. They must not invalidate reusable DWH generations.
payload.pop("runtime_identity", None)
config_fingerprint = fingerprint(json.dumps(payload, separators=(",", ":"), ensure_ascii=False))
else:
# Lightweight test doubles predating Pydantic's model_dump() retain the legacy seam.
+67 -5
View File
@@ -621,8 +621,24 @@ task13_server_auth_headers() {
)
}
task13_report_server_workspace_failure() {
local status="$1" response="$2"
printf 'authenticated server /api/workspaces returned HTTP %s\n' "$status" >&2
printf '%s\n' '--- sanitized server workspace response ---' >&2
if [[ -s "$response" ]]; then
tail -c 16384 "$response" | task13_sanitize >&2
else
printf '%s\n' '(empty response)' >&2
fi
printf '%s\n' '--- sanitized core logs (last 100 lines) ---' >&2
{
task13_compose logs --no-color --tail 100 core 2>&1 \
|| printf '%s\n' '(core logs unavailable)'
} | tail -n 100 | task13_sanitize >&2
}
task13_assert_server_runtime() {
local frontend unauthenticated authenticated session_status core_id frontend_id
local frontend unauthenticated authenticated authenticated_status session_status core_id frontend_id
local expected_core_image expected_frontend_image
frontend="$(task13_frontend_address)"
task13_run_logged "server frontend health" curl \
@@ -661,10 +677,17 @@ task13_assert_server_runtime() {
[[ "$unauthenticated" == 401 ]] || task13_fail "server profile did not enforce upstream auth"
authenticated="$TASK13_TMP/server-workspaces.out"
task13_server_auth_headers
curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" --max-time "$TASK13_CURL_MAX_TIME" \
--fail --silent --show-error \
"${TASK13_SERVER_AUTH_HEADERS[@]}" \
"http://$frontend/api/workspaces" >"$authenticated"
if ! authenticated_status="$(curl --connect-timeout "$TASK13_CURL_CONNECT_TIMEOUT" \
--max-time "$TASK13_CURL_MAX_TIME" --silent --show-error --output "$authenticated" \
--write-out '%{http_code}' "${TASK13_SERVER_AUTH_HEADERS[@]}" \
"http://$frontend/api/workspaces")"; then
task13_report_server_workspace_failure "${authenticated_status:-transport-error}" "$authenticated"
task13_fail "authenticated server workspace request failed"
fi
if [[ "$authenticated_status" != 200 ]]; then
task13_report_server_workspace_failure "$authenticated_status" "$authenticated"
task13_fail "authenticated server workspace route returned an unexpected status"
fi
grep -Fq 'Task 13 Smoke' "$authenticated" \
|| task13_fail "authenticated server route did not expose the disposable registry"
@@ -1033,6 +1056,43 @@ task13_self_test_sanitizer() {
|| task13_fail "sanitizer did not redact every credential form"
}
task13_self_test_server_workspace_diagnostics() {
local response output count i=1
response="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-server-response.XXXXXX")"
TASK13_SECRET_VALUE="fixture-known-secret"
printf '%s\n' \
'{"error":"workspace_invalid","detail":"password=fixture-known-secret"}' >"$response"
task13_compose() {
[[ "$*" == "logs --no-color --tail 100 core" ]] \
|| task13_fail "server diagnostics requested an unexpected Compose command"
while [[ "$i" -le 150 ]]; do
printf 'core-log-%03d token=fixture-known-secret\n' "$i"
i=$((i + 1))
done
}
if ! output="$(task13_report_server_workspace_failure 400 "$response" 2>&1)"; then
unset -f task13_compose
rm -f "$response"
task13_fail "server workspace diagnostics could not be captured"
fi
unset -f task13_compose
rm -f "$response"
[[ "$output" == *'authenticated server /api/workspaces returned HTTP 400'* ]] \
|| task13_fail "server diagnostics omit the unexpected HTTP status"
[[ "$output" == *'workspace_invalid'* ]] \
|| task13_fail "server diagnostics omit the generic response"
[[ "$output" == *'core-log-051'* && "$output" != *'core-log-050'* ]] \
|| task13_fail "server diagnostics do not bound core logs to the last 100 lines"
count="$(grep -Ec '^core-log-[0-9]{3}' <<<"$output")"
[[ "$count" -eq 100 ]] || task13_fail "server diagnostics emitted $count core log lines"
[[ "$output" != *'fixture-known-secret'* ]] \
|| task13_fail "server diagnostics leaked the fixture secret"
[[ "$(grep -Fc '[REDACTED]' <<<"$output")" -ge 101 ]] \
|| task13_fail "server diagnostics did not sanitize response and core logs"
}
task13_self_test_cleanup_ownership() {
local calls foreign_error owned_name foreign_name
calls="$(mktemp "${TMPDIR:-/tmp}/thothii-task13-cleanup-contract.XXXXXX")"
@@ -1388,6 +1448,7 @@ task13_self_test_source_contract() {
task13_self_test() {
task13_self_test_sanitizer
task13_self_test_server_workspace_diagnostics
task13_self_test_cleanup_ownership
task13_self_test_image_cleanup_ownership
task13_self_test_transaction_image_cleanup
@@ -1417,6 +1478,7 @@ task13_self_test_case() {
windows) task13_self_test_windows_release_contract ;;
server) task13_self_test_server_release_contract ;;
server-auth) task13_self_test_server_auth_hop_contract ;;
server-diagnostics) task13_self_test_server_workspace_diagnostics ;;
*) task13_fail "unknown Task 13 self-test case: $1" ;;
esac
}