From e0950f5ed446496dc1a528f2400685c90961113d Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 11 Aug 2026 09:01:47 +0200 Subject: [PATCH] refactor: share registry runtime configuration leases --- backend/src/tht/tht-runner.ts | 144 ++------ .../src/workspaces/runtime-config-lease.ts | 322 ++++++++++++++++++ backend/src/workspaces/runtime-renderer.ts | 2 + .../workspace-runtime-config-lease.test.ts | 85 +++++ .../test/workspace-runtime-handoff.test.ts | 4 +- 5 files changed, 447 insertions(+), 110 deletions(-) create mode 100644 backend/src/workspaces/runtime-config-lease.ts create mode 100644 backend/test/workspace-runtime-config-lease.test.ts diff --git a/backend/src/tht/tht-runner.ts b/backend/src/tht/tht-runner.ts index db46c031..1a44bdd2 100644 --- a/backend/src/tht/tht-runner.ts +++ b/backend/src/tht/tht-runner.ts @@ -5,16 +5,11 @@ import { openSync, readFileSync, readSync, realpathSync, statSync, unlinkSync, writeFileSync, } from "node:fs"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; -import { parseAllDocuments } from "yaml"; import { clearPrincipalEnvironment, principalEnvironment, type PrincipalContext } from "../auth/principal.js"; import { secretValue, type SecretBundleConfig } from "../config/secret-bundle.js"; -import { resolveRuntimeBindings } from "../workspaces/bindings.js"; -import { - renderRuntimeConfig, - type RuntimeInstallationOverlay, - type RuntimePaths, - type SemanticRuntimeConfig, -} from "../workspaces/runtime-renderer.js"; +import { type SemanticRuntimeConfig } from "../workspaces/runtime-renderer.js"; +import { WorkspaceRuntimeConfigLeaseFactory, type RuntimeConfigLease } from "../workspaces/runtime-config-lease.js"; +export type { RuntimeConfigLease } from "../workspaces/runtime-config-lease.js"; import { parseWorkspaceYaml, validateOperationalWorkspace, @@ -32,13 +27,6 @@ export interface ThtConfig extends SecretBundleConfig { qdrantRequest?: typeof fetch; } -export interface RuntimeConfigLease { - path: string; - workspaceId: string; - workspaceRevision: string; - release(): void; -} - export interface SessionRow { id: string; status: string; @@ -98,11 +86,19 @@ interface RuntimeSnapshot { export class ThtRunner { private readonly runtimeSnapshots = new Map(); + private readonly runtimeLeases = new Map(); + private runtimeLeaseFactory?: WorkspaceRuntimeConfigLeaseFactory; constructor(private cfg: ThtConfig, private principal?: PrincipalContext) {} /** Bind one trusted request principal to every child spawned by this runner. */ - withPrincipal(principal: PrincipalContext): ThtRunner { return new ThtRunner(this.cfg, principal); } + withPrincipal(principal: PrincipalContext): ThtRunner { + const runner = new ThtRunner(this.cfg, principal); + // Registry config publication is process-scoped: principal-bound runners must share the + // lease factory so two concurrent callers cannot release one another's deterministic path. + runner.runtimeLeaseFactory = this.runtimeLeaseFactory; + return runner; + } /** * Resolve the `-c ` args. A named workspace MUST exist: silently falling @@ -113,7 +109,7 @@ export class ThtRunner { if (workspaceConfigPath) { if (isAbsolute(workspaceConfigPath)) { if (this.runtimeSnapshots.has(workspaceConfigPath)) this.assertTrustedRuntimeSnapshot(workspaceConfigPath); - else this.assertWorkspaceSnapshot(workspaceConfigPath); + else if (!this.runtimeLeases.has(workspaceConfigPath)) this.assertWorkspaceSnapshot(workspaceConfigPath); return ["-c", workspaceConfigPath]; } if (workspaceConfigPath.includes("/")) { @@ -138,102 +134,29 @@ export class ThtRunner { || !match ) throw new Error("config path is not a trusted runtime snapshot"); const entry = lstatSync(path); - if (!entry.isFile() || entry.isSymbolicLink()) { + if (!entry.isFile() || entry.isSymbolicLink() || entry.nlink !== 1 || lstatSync(dirname(path)).isSymbolicLink()) { throw new Error("config path is not a trusted runtime snapshot"); } return { workspaceRevision: match[1], workspaceId: match[2] }; } - private readCanonicalWorkspaceSnapshot(path: string): { - workspace: ReturnType; - workspaceId: string; - workspaceRevision: string; - revisionContentRoot: string; - } { - const identity = this.assertWorkspaceSnapshot(path); - const fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW); - try { - const before = fstatSync(fd); - if (!before.isFile()) throw new Error("workspace snapshot is not a file"); - const source = readFileSync(fd, "utf8"); - const after = fstatSync(fd); - if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size) { - throw new Error("workspace snapshot changed while reading"); - } - const workspace = validateOperationalWorkspace(parseWorkspaceYaml(source)); - if (workspace.workspace.id !== identity.workspaceId) { - throw new Error("workspace snapshot identity does not match its path"); - } - return { workspace, ...identity, revisionContentRoot: dirname(path) }; - } finally { - closeSync(fd); - } - } - - private runtimePaths(workspaceId: string): RuntimePaths { - if (!this.cfg.dataRoot || !isAbsolute(this.cfg.dataRoot)) { - throw new Error("registry workspace runtime requires an absolute data root"); - } - // The portable stack persists one `sessions` store at /sessions. Keep every - // workspace's mutable harness roots below that mounted boundary. - const root = join(this.cfg.dataRoot, "sessions", workspaceId); - return { - sessions: join(root, "sessions"), - artifacts: join(root, "artifacts"), - indexes: join(root, "indexes"), - }; - } - - private installationOverlay(): RuntimeInstallationOverlay { - const path = isAbsolute(this.cfg.configPath) - ? this.cfg.configPath - : resolve(this.cfg.harnessDir, this.cfg.configPath); - if (!existsSync(path)) return {}; - const documents = parseAllDocuments(readFileSync(path, "utf8"), { uniqueKeys: true }); - if (documents.length !== 1) throw new Error("installation config must contain one YAML document"); - const document = documents[0]; - if (document.errors.length > 0 || document.warnings.length > 0) { - throw new Error("installation config contains invalid YAML"); - } - const parsed = document.toJSON(); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - throw new Error("installation config must be a YAML mapping"); - } - const source = parsed as Record; - return { - ...(source.session_storage === undefined ? {} : { session_storage: source.session_storage }), - ...(source.profile === undefined ? {} : { profile: source.profile }), - }; - } - - /** Render one immutable canonical registry revision into a backend-owned harness config. */ + /** Render the pinned registry revision through the shared deterministic lease. */ acquireWorkspaceRuntime(workspaceConfigPath: string): RuntimeConfigLease { - const canonical = this.readCanonicalWorkspaceSnapshot(workspaceConfigPath); - const bindings = resolveRuntimeBindings( - canonical.workspace, - process.env, - this.cfg.secretRoots ?? [], - ); - const config = renderRuntimeConfig( - canonical.workspace, - bindings, - this.runtimePaths(canonical.workspaceId), - canonical, - this.installationOverlay(), - this.cfg.semanticRuntime, - ); - const path = this.createRuntimeSnapshot(config); - let released = false; - return { - path, - workspaceId: canonical.workspaceId, - workspaceRevision: canonical.workspaceRevision, - release: () => { - if (released) return; - released = true; - this.cleanupRuntimeSnapshot(path); - }, - }; + return this.runtimeConfigLeases().acquireSession(workspaceConfigPath); + } + + private runtimeConfigLeases(): WorkspaceRuntimeConfigLeaseFactory { + if (this.runtimeLeaseFactory) return this.runtimeLeaseFactory; + this.runtimeLeaseFactory = new WorkspaceRuntimeConfigLeaseFactory({ + dataRoot: this.cfg.dataRoot ?? "", + runtimeSnapshotRoot: this.cfg.runtimeSnapshotRoot ?? "", + harnessDir: this.cfg.harnessDir, + configPath: this.cfg.configPath, + secretRoots: this.cfg.secretRoots, + env: process.env, + semanticRuntime: this.cfg.semanticRuntime, + }); + return this.runtimeLeaseFactory; } private runtimeSnapshotDirectory(): string { @@ -366,6 +289,7 @@ export class ThtRunner { if ( workspaceConfigPath && isAbsolute(workspaceConfigPath) && !this.runtimeSnapshots.has(workspaceConfigPath) + && !this.runtimeLeases.has(workspaceConfigPath) ) { let runtime: RuntimeConfigLease; try { @@ -373,7 +297,11 @@ export class ThtRunner { } catch (error) { return Promise.reject(error); } - return this.run(args, runtime.path, timeoutMs).finally(runtime.release); + this.runtimeLeases.set(runtime.path, runtime); + return this.run(args, runtime.path, timeoutMs).finally(() => { + this.runtimeLeases.delete(runtime.path); + runtime.release(); + }); } return new Promise((resolve) => { const env: NodeJS.ProcessEnv = { ...process.env }; diff --git a/backend/src/workspaces/runtime-config-lease.ts b/backend/src/workspaces/runtime-config-lease.ts new file mode 100644 index 00000000..fb66ef96 --- /dev/null +++ b/backend/src/workspaces/runtime-config-lease.ts @@ -0,0 +1,322 @@ +import { createHash, randomUUID } from "node:crypto"; +import { + chmodSync, closeSync, constants as fsConstants, fchmodSync, fstatSync, fsyncSync, lstatSync, + existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, + writeSync, +} from "node:fs"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { parseAllDocuments } from "yaml"; +import { resolveRuntimeBindings } from "./bindings.js"; +import { + renderRuntimeConfig, + type RuntimeInstallationOverlay, + type RuntimePaths, + type SemanticRuntimeConfig, +} from "./runtime-renderer.js"; +import { parseWorkspaceYaml, validateOperationalWorkspace, type WorkspaceDescriptor } from "./schema.js"; + +export interface RuntimeConfigLease { + path: string; + manifestPath: string; + workspaceId: string; + workspaceRevision: string; + release(): void; +} + +export interface MaintenanceRuntimeInput { + /** The immutable registry snapshot. `workspaceConfigPath` is accepted for callers using that name. */ + snapshotPath?: string; + workspaceConfigPath?: string; +} + +export interface WorkspaceRuntimeConfigLeaseFactoryInput { + dataRoot: string; + runtimeSnapshotRoot: string; + harnessDir: string; + configPath: string; + secretRoots?: readonly string[]; + env?: NodeJS.ProcessEnv; + installationOverlay?: RuntimeInstallationOverlay; + semanticRuntime: SemanticRuntimeConfig; +} + +interface SnapshotIdentity { + workspace: WorkspaceDescriptor; + workspaceId: string; + workspaceRevision: string; + revisionContentRoot: string; + digest: string; +} +interface PublishedIdentity { + path: string; + manifestPath: string; + workspaceId: string; + workspaceRevision: string; + digest: string; + content: string; + manifest: string; + refs: number; +} + +const DEFAULT_SEMANTIC_RUNTIME: SemanticRuntimeConfig = { + internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434", + internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024, +}; + +/** Normalize the installation HTTP private-host policy once, before rendering. */ +export function normalizePrivateHostAllowlist(value: string | readonly string[] | undefined): string[] { + const values: readonly string[] = value === undefined ? [] : typeof value === "string" ? (value === "" ? [] : value.split(",")) : [...value]; + if (values.length > 32) throw new Error("HTTP private host allowlist has too many entries"); + const result: string[] = []; + for (const host of values) { + if ( + typeof host !== "string" || host.length === 0 || host.length > 253 || host !== host.toLowerCase() + || host.endsWith(".") || host.includes(" ") || host.includes("\t") + || host.includes("*") || host.includes("/") || host.includes("_") + || /^[0-9.]+$/.test(host) || host.includes(":") + ) throw new Error("HTTP private host allowlist contains an invalid hostname"); + const labels = host.split("."); + if (labels.some((label) => label.length === 0 || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label))) { + throw new Error("HTTP private host allowlist contains an invalid hostname"); + } + if (result.includes(host)) throw new Error("HTTP private host allowlist contains a duplicate hostname"); + result.push(host); + } + return result; +} + + +export const normalizeHttpPrivateHostAllowlist = normalizePrivateHostAllowlist; + +function parseInstallationOverlay(path: string): RuntimeInstallationOverlay { + if (!isAbsolute(path) || !existsSync(path)) return {}; + const docs = parseAllDocuments(readFileSync(path, "utf8"), { uniqueKeys: true }); + if (docs.length !== 1 || docs[0].errors.length || docs[0].warnings.length) { + throw new Error("installation config contains invalid YAML"); + } + const value = docs[0].toJSON(); + if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("installation config must be a YAML mapping"); + const source = value as Record; + return { + ...(source.session_storage === undefined ? {} : { session_storage: source.session_storage }), + ...(source.profile === undefined ? {} : { profile: source.profile }), + }; +} + +function isSafeMode(mode: number, expected: number): boolean { + return (mode & 0o777) === expected; +} +function digest(data: string | Buffer): string { return createHash("sha256").update(data).digest("hex"); } +function regularNoLink(path: string, mode?: number): ReturnType { + const entry = lstatSync(path); + if (!entry.isFile() || entry.isSymbolicLink() || entry.nlink !== 1 || (mode !== undefined && !isSafeMode(entry.mode, mode))) { + throw new Error("runtime config destination is not trusted"); + } + return entry; +} +function fsyncDirectory(path: string): void { + const fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY); + try { fsyncSync(fd); } finally { closeSync(fd); } +} + +export class WorkspaceRuntimeConfigLeaseFactory { + private readonly env: NodeJS.ProcessEnv; + private readonly secretRoots: readonly string[]; + private readonly installation: RuntimeInstallationOverlay; + private readonly published = new Map(); + + constructor(private readonly input: WorkspaceRuntimeConfigLeaseFactoryInput) { + if (!isAbsolute(input.dataRoot) || !isAbsolute(input.runtimeSnapshotRoot)) { + throw new Error("workspace runtime roots must be absolute"); + } + mkdirSync(input.runtimeSnapshotRoot, { recursive: true, mode: 0o700 }); + const snapshotRoot = lstatSync(input.runtimeSnapshotRoot); + if (!snapshotRoot.isDirectory() || snapshotRoot.isSymbolicLink() || (snapshotRoot.mode & 0o077) !== 0) { + throw new Error("runtime snapshot root is not trusted"); + } + this.env = { ...(input.env ?? process.env) }; + this.secretRoots = [...(input.secretRoots ?? [])]; + const configPath = isAbsolute(input.configPath) ? input.configPath : resolve(input.harnessDir, input.configPath); + const suppliedAllowlist = input.installationOverlay?.egress?.http_private_host_allowlist; + this.installation = { + ...parseInstallationOverlay(configPath), + ...(input.installationOverlay ?? {}), + egress: { http_private_host_allowlist: normalizePrivateHostAllowlist( + suppliedAllowlist ?? this.env.THT_HTTP_PRIVATE_HOST_ALLOWLIST, + ) }, + } as RuntimeInstallationOverlay; + } + + acquireSession(snapshotPath: string): RuntimeConfigLease { return this.acquire(snapshotPath); } + acquireMaintenance(input: MaintenanceRuntimeInput): RuntimeConfigLease { + const snapshotPath = input.snapshotPath ?? input.workspaceConfigPath; + if (!snapshotPath) throw new Error("maintenance runtime snapshot is required"); + return this.acquire(snapshotPath); + } + + private acquire(snapshotPath: string): RuntimeConfigLease { + const snapshot = this.readSnapshot(snapshotPath); + const paths = this.runtimePaths(snapshot.workspaceId); + const rendered = renderRuntimeConfig( + snapshot.workspace, + resolveRuntimeBindings(snapshot.workspace, this.env, this.secretRoots), + paths, + snapshot, + this.installation, + this.input.semanticRuntime ?? DEFAULT_SEMANTIC_RUNTIME, + ); + const configPath = join(this.input.dataRoot, "sessions", snapshot.workspaceId, "preprocessing", "runtime-config", `${snapshot.workspaceRevision}.yaml`); + const manifestPath = join(dirname(configPath), `${snapshot.workspaceRevision}.manifest.json`); + const renderedDigest = digest(rendered); + const existing = this.published.get(configPath); + if (existing) { + if (existing.digest !== renderedDigest) throw new Error("same-revision runtime configuration changed"); + try { + regularNoLink(configPath, 0o400); + regularNoLink(manifestPath, 0o600); + const manifestBytes = `${JSON.stringify({ + workspace_id: snapshot.workspaceId, + workspace_revision: snapshot.workspaceRevision, + config_sha256: renderedDigest, + })}\n`; + if (readFileSync(configPath, "utf8") !== rendered || readFileSync(manifestPath, "utf8") !== manifestBytes) { + throw new Error("same-revision runtime configuration changed"); + } + } catch (error) { + if (error instanceof Error && /same-revision/.test(error.message)) throw error; + throw new Error("same-revision runtime configuration changed"); + } + existing.refs += 1; + return this.lease(existing); + } + this.ensureDestinationDirectory(dirname(configPath)); + this.publish(configPath, manifestPath, rendered, { + workspace_id: snapshot.workspaceId, + workspace_revision: snapshot.workspaceRevision, + config_sha256: renderedDigest, + }); + const identity: PublishedIdentity = { + path: configPath, manifestPath, workspaceId: snapshot.workspaceId, + workspaceRevision: snapshot.workspaceRevision, digest: renderedDigest, content: rendered, + manifest: `${JSON.stringify({ workspace_id: snapshot.workspaceId, workspace_revision: snapshot.workspaceRevision, config_sha256: renderedDigest })}\n`, refs: 1, + }; + this.published.set(configPath, identity); + return this.lease(identity); + } + + private lease(identity: PublishedIdentity): RuntimeConfigLease { + let released = false; + return { + path: identity.path, manifestPath: identity.manifestPath, + workspaceId: identity.workspaceId, workspaceRevision: identity.workspaceRevision, + release: () => { + if (released) return; + released = true; + const current = this.published.get(identity.path); + if (!current) return; + current.refs -= 1; + if (current.refs > 0) return; + this.published.delete(identity.path); + this.removeIfUnchanged(identity.manifestPath, identity.manifest, 0o600); + this.removeIfUnchanged(identity.path, identity.content, 0o400); + }, + }; + } + + private removeIfUnchanged(path: string, expected: string, mode: number): void { + try { + regularNoLink(path, mode); + if (readFileSync(path, "utf8") === expected) unlinkSync(path); + } catch { /* never remove a replaced or untrusted destination */ } + } + + private runtimePaths(workspaceId: string): RuntimePaths { + const root = join(this.input.dataRoot, "sessions", workspaceId); + return { sessions: join(root, "sessions"), artifacts: join(root, "artifacts"), indexes: join(root, "indexes") }; + } + + private snapshotRoots(): string[] { + return [this.input.runtimeSnapshotRoot, dirname(this.input.runtimeSnapshotRoot)]; + } + + private readSnapshot(path: string): SnapshotIdentity { + if (!isAbsolute(path)) throw new Error("workspace snapshot path must be absolute"); + let match: RegExpExecArray | null = null; + for (const candidate of this.snapshotRoots()) { + const rel = relative(candidate, path); + const found = /^([0-9a-f]{40})\/([a-z][a-z0-9-]{2,62})\.yaml$/.exec(rel); + if (found && !rel.startsWith("..") && !isAbsolute(rel)) { match = found; break; } + } + if (!match) throw new Error("config path is not a trusted runtime snapshot"); + const revisionDirectory = lstatSync(dirname(path)); + if (!revisionDirectory.isDirectory() || revisionDirectory.isSymbolicLink()) { + throw new Error("workspace snapshot parent is not trusted"); + } + const fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW); + try { + const before = fstatSync(fd); + if (!before.isFile() || before.nlink !== 1) throw new Error("workspace snapshot is not a trusted file"); + const source = readFileSync(fd, "utf8"); + const after = fstatSync(fd); + if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size) throw new Error("workspace snapshot changed while reading"); + const workspace = validateOperationalWorkspace(parseWorkspaceYaml(source)); + if (workspace.workspace.id !== match[2]) throw new Error("workspace snapshot identity does not match its path"); + return { workspace, workspaceId: match[2], workspaceRevision: match[1], revisionContentRoot: dirname(path), digest: digest(source) }; + } finally { closeSync(fd); } + } + + private ensureDestinationDirectory(path: string): void { + const root = this.input.dataRoot; + mkdirSync(root, { recursive: true, mode: 0o700 }); + const rootEntry = lstatSync(root); + if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) { + throw new Error("runtime config destination is not trusted"); + } + chmodSync(root, 0o700); + const components = relative(root, path).split("/").filter(Boolean); + let current = root; + for (const component of components) { + current = join(current, component); + mkdirSync(current, { recursive: true, mode: 0o700 }); + const entry = lstatSync(current); + if (!entry.isDirectory() || entry.isSymbolicLink() || (entry.mode & 0o077) !== 0) throw new Error("runtime config destination is not trusted"); + } + } + + private publish(path: string, manifestPath: string, content: string, manifest: Record): void { + const manifestBytes = `${JSON.stringify(manifest)}\n`; + const configExists = (() => { try { regularNoLink(path, 0o400); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; } })(); + const manifestExists = (() => { try { regularNoLink(manifestPath, 0o600); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; throw error; } })(); + if (configExists || manifestExists) { + if (!configExists || !manifestExists || readFileSync(path, "utf8") !== content || readFileSync(manifestPath, "utf8") !== manifestBytes) { + throw new Error("same-revision runtime configuration changed"); + } + return; + } + const writeAtomic = (destination: string, bytes: string, mode: number) => { + const staging = `${destination}.staging-${randomUUID()}`; + const fd = openSync(staging, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600); + try { + try { + writeSync(fd, bytes, undefined, "utf8"); + fchmodSync(fd, mode); fsyncSync(fd); + } catch (error) { + try { unlinkSync(staging); } catch { /* retain the original durability error */ } + throw error; + } finally { closeSync(fd); } + } catch (error) { + try { unlinkSync(staging); } catch { /* retain the original durability error */ } + throw error; + } + try { renameSync(staging, destination); fsyncDirectory(dirname(destination)); } + catch (error) { try { unlinkSync(staging); } catch { /* preserve original failure */ } throw error; } + regularNoLink(destination, mode); + }; + try { writeAtomic(path, content, 0o400); writeAtomic(manifestPath, manifestBytes, 0o600); } + catch (error) { + this.removeIfUnchanged(path, content, 0o400); + this.removeIfUnchanged(manifestPath, manifestBytes, 0o600); + throw error; + } + } +} diff --git a/backend/src/workspaces/runtime-renderer.ts b/backend/src/workspaces/runtime-renderer.ts index c120464f..22e0e08d 100644 --- a/backend/src/workspaces/runtime-renderer.ts +++ b/backend/src/workspaces/runtime-renderer.ts @@ -24,6 +24,7 @@ export interface RuntimeRenderContext extends RuntimeIdentity { export interface RuntimeInstallationOverlay { session_storage?: unknown; profile?: unknown; + egress?: { http_private_host_allowlist: readonly string[] }; } export interface SemanticRuntimeConfig { @@ -253,6 +254,7 @@ export function renderRuntimeConfig( ...(installation.session_storage === undefined ? {} : { session_storage: installation.session_storage }), ...(installation.profile === undefined ? {} : { profile: installation.profile }), + ...(installation.egress === undefined ? {} : { egress: installation.egress }), language: descriptor.workspace.language, database, vectors: { diff --git a/backend/test/workspace-runtime-config-lease.test.ts b/backend/test/workspace-runtime-config-lease.test.ts new file mode 100644 index 00000000..a93365c5 --- /dev/null +++ b/backend/test/workspace-runtime-config-lease.test.ts @@ -0,0 +1,85 @@ +import { test, expect } from "vitest"; +import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { WorkspaceRuntimeConfigLeaseFactory } from "../src/workspaces/runtime-config-lease.js"; + +const commit = "a".repeat(40); +const workspace = "abc"; +const descriptor = `workspace: + schema_version: 3 + id: ${workspace} + name: Lease + language: en +dwh: + engine: postgres + database: analytics + schema: mart + supported_transports: [postgres_direct] +semantic_index: + vector_store: + engine: qdrant + collection: abc + dimensions: 1024 + distance: cosine + embedding: + provider: ollama_internal + model: qwen3-embedding:0.6b + dimensions: 1024 +llm_policy: + allowed: [zai/glm-5.2] +`; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), "runtime-config-lease-")); + const snapshots = join(root, "snapshots"); + const snapshotPath = join(snapshots, commit, `${workspace}.yaml`); + const dataRoot = join(root, "data"); + const harness = join(root, "harness"); + mkdirSync(join(snapshots, commit), { recursive: true, mode: 0o700 }); + chmodSync(snapshots, 0o700); + mkdirSync(harness); + writeFileSync(snapshotPath, descriptor, { mode: 0o400 }); + const secret = join(root, "password"); + writeFileSync(secret, "secret", { mode: 0o600 }); + const configPath = join(harness, "config.yaml"); + writeFileSync(configPath, "profile: test\n"); + const factory = new WorkspaceRuntimeConfigLeaseFactory({ + dataRoot, runtimeSnapshotRoot: snapshots, harnessDir: harness, configPath, + env: { + THT_WS_ABC_DWH_TRANSPORT: "postgres_direct", THT_WS_ABC_DWH_HOST: "dwh", + THT_WS_ABC_DWH_PORT: "5432", THT_WS_ABC_DWH_USER: "reader", + THT_WS_ABC_DWH_PASSWORD_FILE: secret, + }, secretRoots: [root], semanticRuntime: { + internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434", + internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024, + }, + }); + return { root, snapshotPath, factory }; +} + +test("session and maintenance share deterministic bytes and path", () => { + const f = fixture(); + try { + const session = f.factory.acquireSession(f.snapshotPath); + const maintenance = f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath }); + expect(session.path).toBe(maintenance.path); + expect(readFileSync(session.path, "utf8")).toBe(readFileSync(maintenance.path, "utf8")); + expect(lstatSync(session.path).mode & 0o777).toBe(0o400); + expect(existsSync(join(dirname(session.path), `${commit}.manifest.json`))).toBe(true); + session.release(); maintenance.release(); + expect(existsSync(session.path)).toBe(false); + } finally { rmSync(f.root, { recursive: true, force: true }); } +}); + +test("same revision changed bytes are refused", () => { + const f = fixture(); + try { + const first = f.factory.acquireSession(f.snapshotPath); + chmodSync(first.path, 0o600); + writeFileSync(first.path, "changed", { mode: 0o600 }); + chmodSync(first.path, 0o400); + expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/changed|mismatch|trusted/i); + first.release(); + } finally { rmSync(f.root, { recursive: true, force: true }); } +}); diff --git a/backend/test/workspace-runtime-handoff.test.ts b/backend/test/workspace-runtime-handoff.test.ts index c252250d..e5f98c3a 100644 --- a/backend/test/workspace-runtime-handoff.test.ts +++ b/backend/test/workspace-runtime-handoff.test.ts @@ -180,7 +180,7 @@ test("separate runtime leases hand off byte-identical revision Evidence configs ); try { - expect(first.path).not.toBe(second.path); + expect(first.path).toBe(second.path); const firstYaml = readFileSync(first.path, "utf8"); const secondYaml = readFileSync(second.path, "utf8"); expect(secondYaml).toBe(firstYaml); @@ -212,7 +212,7 @@ test("separate runtime leases hand off byte-identical revision Evidence configs } first.release(); - expect(existsSync(first.path)).toBe(false); + expect(existsSync(first.path)).toBe(true); expect(existsSync(second.path)).toBe(true); second.release(); expect(existsSync(second.path)).toBe(false);