fix: harden workspace runtime snapshots
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { test, expect, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, rmSync, symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { ThtRunner } from "../src/tht/tht-runner.js";
|
||||
@@ -170,10 +173,110 @@ test("buildArgv appends -c AFTER the subcommand (never a global -c)", () => {
|
||||
});
|
||||
|
||||
test("buildArgv passes an absolute immutable snapshot after the tht subcommand", () => {
|
||||
const r = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" });
|
||||
expect(r.buildArgv(["session", "new"], "/data/workspace-registry/snapshots/a/psd-clinical.yaml")).toEqual([
|
||||
"session", "new", "-c", "/data/workspace-registry/snapshots/a/psd-clinical.yaml",
|
||||
]);
|
||||
const root = mkdtempSync(join(tmpdir(), "tht-runner-snapshot-"));
|
||||
const snapshotRoot = join(root, "snapshots", "runtime");
|
||||
mkdirSync(snapshotRoot, { recursive: true, mode: 0o700 });
|
||||
const r = new ThtRunner({
|
||||
thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml", runtimeSnapshotRoot: snapshotRoot,
|
||||
});
|
||||
try {
|
||||
const snapshot = r.createRuntimeSnapshot("language: en\n");
|
||||
expect(lstatSync(snapshot).isFile()).toBe(true);
|
||||
expect(lstatSync(snapshot).mode & 0o777).toBe(0o400);
|
||||
expect(r.buildArgv(["session", "new"], snapshot)).toEqual([
|
||||
"session", "new", "-c", snapshot,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("absolute config paths must be unmodified runner-created snapshots", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "tht-runner-snapshot-"));
|
||||
const snapshotRoot = join(root, "snapshots", "runtime");
|
||||
const outside = join(root, "outside.yaml");
|
||||
mkdirSync(snapshotRoot, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(outside, "language: en\n");
|
||||
const r = new ThtRunner({
|
||||
thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml", runtimeSnapshotRoot: snapshotRoot,
|
||||
});
|
||||
try {
|
||||
expect(() => r.buildArgv(["session", "new"], "/tmp/untrusted.yaml"))
|
||||
.toThrow(/trusted runtime snapshot/i);
|
||||
expect(() => r.buildArgv(["session", "new"], outside))
|
||||
.toThrow(/trusted runtime snapshot/i);
|
||||
|
||||
const snapshot = r.createRuntimeSnapshot("language: en\n");
|
||||
chmodSync(snapshot, 0o600);
|
||||
writeFileSync(snapshot, "language: it\n");
|
||||
chmodSync(snapshot, 0o400);
|
||||
expect(() => r.buildArgv(["session", "new"], snapshot))
|
||||
.toThrow(/trusted runtime snapshot/i);
|
||||
|
||||
const symlink = join(snapshotRoot, "symlink.yaml");
|
||||
symlinkSync(outside, symlink);
|
||||
expect(() => r.buildArgv(["session", "new"], symlink))
|
||||
.toThrow(/trusted runtime snapshot/i);
|
||||
|
||||
const directory = join(snapshotRoot, "directory.yaml");
|
||||
mkdirSync(directory);
|
||||
expect(() => r.buildArgv(["session", "new"], directory))
|
||||
.toThrow(/trusted runtime snapshot/i);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runtime snapshots require an absolute configured root", () => {
|
||||
const relativeRoot = `tht-runner-relative-${Date.now()}`;
|
||||
const r = new ThtRunner({
|
||||
thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml", runtimeSnapshotRoot: relativeRoot,
|
||||
});
|
||||
try {
|
||||
expect(() => r.createRuntimeSnapshot("language: en\n")).toThrow(/runtime snapshot root/i);
|
||||
} finally {
|
||||
rmSync(join(process.cwd(), relativeRoot), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runtime snapshots are consumed through a read-only descriptor and cleaned after success", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "tht-runner-snapshot-"));
|
||||
const snapshotRoot = join(root, "snapshots", "runtime");
|
||||
const r = new ThtRunner({
|
||||
thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml", runtimeSnapshotRoot: snapshotRoot,
|
||||
});
|
||||
try {
|
||||
(spawn as any).mockClear();
|
||||
await r.runWithRuntimeSnapshot(["session", "list", "--json"], "language: en\n");
|
||||
const [, argv, options] = (spawn as any).mock.calls[0];
|
||||
expect(argv.slice(-2)).toEqual(["-c", "/dev/fd/3"]);
|
||||
expect(options.stdio).toHaveLength(4);
|
||||
expect(readdirSync(snapshotRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runtime snapshots are cleaned after a failed child", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "tht-runner-snapshot-"));
|
||||
const snapshotRoot = join(root, "snapshots", "runtime");
|
||||
const r = new ThtRunner({
|
||||
thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml", runtimeSnapshotRoot: snapshotRoot,
|
||||
});
|
||||
try {
|
||||
(spawn as any).mockImplementationOnce(() => {
|
||||
const ch: any = new EventEmitter();
|
||||
ch.stdout = new EventEmitter();
|
||||
ch.stderr = new EventEmitter();
|
||||
queueMicrotask(() => ch.emit("close", 1));
|
||||
return ch;
|
||||
});
|
||||
const result = await r.runWithRuntimeSnapshot(["session", "list", "--json"], "language: en\n");
|
||||
expect(result.code).toBe(1);
|
||||
expect(readdirSync(snapshotRoot)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("sqlPreview argv has no positional file — uses --session to resolve path", async () => {
|
||||
|
||||
@@ -42,6 +42,7 @@ const directBindings: RuntimeBindings = {
|
||||
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
||||
THT_WS_PSD_CLINICAL_DWH_USER: "thoth_reader",
|
||||
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password",
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca.pem",
|
||||
},
|
||||
},
|
||||
vector: {
|
||||
@@ -52,6 +53,7 @@ const directBindings: RuntimeBindings = {
|
||||
THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_USER: "vector_reader",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: "/run/secrets/vector-password",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE: "/run/secrets/vector-ca.pem",
|
||||
},
|
||||
},
|
||||
embedding: {
|
||||
@@ -74,12 +76,14 @@ test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
|
||||
schema: "datawarehouse",
|
||||
user: "thoth_reader",
|
||||
password_file: "/run/secrets/dwh-password",
|
||||
ssl_ca_file: "/run/secrets/dwh-ca.pem",
|
||||
transport: "direct",
|
||||
},
|
||||
vector_db: {
|
||||
host: "vector.internal",
|
||||
schema: "datawarehouse",
|
||||
password_file: "/run/secrets/vector-password",
|
||||
ssl_ca_file: "/run/secrets/vector-ca.pem",
|
||||
},
|
||||
embeddings: {
|
||||
base_url: "http://embedding.internal:11434",
|
||||
@@ -92,6 +96,28 @@ test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
|
||||
expect(yaml).toContain("schema: datawarehouse");
|
||||
});
|
||||
|
||||
test("omits direct TLS fields when binding validation did not retain a file path", () => {
|
||||
const dwhValues = { ...directBindings.dwh.values };
|
||||
const vectorValues = { ...directBindings.vector.values };
|
||||
delete dwhValues.THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE;
|
||||
delete vectorValues.THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE;
|
||||
const yaml = renderRuntimeConfig(workspace, {
|
||||
...directBindings,
|
||||
dwh: {
|
||||
...directBindings.dwh,
|
||||
values: dwhValues,
|
||||
},
|
||||
vector: {
|
||||
...directBindings.vector,
|
||||
values: vectorValues,
|
||||
},
|
||||
}, paths);
|
||||
const rendered = parse(yaml);
|
||||
|
||||
expect(rendered.database).not.toHaveProperty("ssl_ca_file");
|
||||
expect(rendered.vector_db).not.toHaveProperty("ssl_ca_file");
|
||||
});
|
||||
|
||||
test("renders REST bindings through the legacy rest sections without secret values", () => {
|
||||
const yaml = renderRuntimeConfig(workspace, {
|
||||
...directBindings,
|
||||
|
||||
@@ -102,6 +102,7 @@ test("reports an invalid optional secret file instead of silently dropping it",
|
||||
}, [password.root]);
|
||||
|
||||
expect(result.missing).toContain("THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE");
|
||||
expect(result.values).not.toHaveProperty("THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE");
|
||||
});
|
||||
|
||||
test("rejects a selected transport that the canonical workspace does not support", () => {
|
||||
|
||||
Reference in New Issue
Block a user