fix: render registry workspaces for harness

This commit is contained in:
2026-08-05 16:03:45 +02:00
parent ece9cfda50
commit bd798b1c96
21 changed files with 659 additions and 69 deletions
+12 -6
View File
@@ -1,6 +1,6 @@
# ThothII — Project State
> Starting-point snapshot for new sessions. Last updated: 2026-08-05 (portable deployment decoupled).
> Starting-point snapshot for new sessions. Last updated: 2026-08-05 (Task 13 fix round 3/5).
> Point a fresh session here ("read PROJECT_STATE.md") before substantial work.
## Unified deployment release gate — Task 13 (2026-08-05)
@@ -39,11 +39,17 @@
writable parent bind while protected/tracked sources remain separate read-only mounts. Clean
empty-root render/setup and wrong-service/value/mount mutations are green. The corrected server
one-shot built and started both healthy services from an empty Pi-state root, then stopped at an
incorrectly addressed authenticated frontend hop; the trusted-hop fixture correction is
deterministic-only. The corrected rollback one-shot passed runtime/Pi/registry/persistence and
stopped-candidate preflight, then stopped at active-session inventory before mutation. Exact
cleanup passed for both. Full server behavior, compensation/all-sentinel preservation, and
native Windows PowerShell/Docker execution remain explicit release gates.
incorrectly addressed authenticated frontend hop. Fix round 3 adds the exact fourth private
non-admin claim and proves its nginx/backend transformation in a focused auth test. It also
centralizes schema-v2 registry descriptor resolution and secret-safe runtime rendering in
`ThtRunner`, preserving canonical revision identity and durable session roots for inventory,
create/resume/show, SQL, and Pi calls. The fresh update-only one-shot now passes mutation,
automatic `rolled_back` compensation, exact prior-image restoration, unchanged registry head
and mount identities, all four persistence sentinels, post-rollback doctor/workspace checks,
and exact labeled-resource cleanup. The one authorized server invocation was blocked at its
first Docker readiness call by the execution sandbox's socket permission before any Compose
resource could be created, so authenticated workspace/fail-closed session behavior remains an
explicit release gate. Native Windows PowerShell/Docker execution also remains pending.
## Portable deployment decoupling — LIVE 2026-08-05
+10 -6
View File
@@ -141,12 +141,16 @@ Each public smoke has its own 30-minute process-group supervisor with TERM/KILL
an independent 32-minute outer timeout and does not retry a failed command.
Current release status (2026-08-05): clean-root render/setup and the production runtime-binding
resolver contracts are green. The single corrected server-profile run proved image build,
clean-root startup, and core/frontend health, then stopped at a fixture-authenticated frontend
request; its trusted-hop headers are corrected deterministically but were not rerun. The single
corrected rollback run reached runtime/Pi/registry/persistence checks and the stopped-candidate
preflight, then stopped at active-session inventory before mutation. Full server behavior and
bad-Pi compensation with unchanged state therefore remain release gates. Native Windows Docker
resolver contracts are green. The server fixture supplies all four private trusted claims,
including exact non-admin value `0`, and a focused test proves nginx normalization produces the
accepted non-admin backend principal. Canonical schema-v2 registry descriptors now pass through
one backend-owned, secret-safe runtime handoff for inventory and session execution; canonical
identity and durable session/artifact/index roots are retained. The fresh update-only smoke passed
bad-candidate mutation, automatic `rolled_back` compensation, exact prior-image restoration,
unchanged registry/mount identity, all four sentinels, post-rollback doctor/workspace checks, and
exact cleanup. The one authorized server-smoke invocation was denied access to the Docker socket
by its execution sandbox before startup, so the complete authenticated workspace and fail-closed
session assertions still require a fresh authorized release run. Native Windows Docker
Desktop/WSL2 remains a separate manual/self-hosted gate.
The deterministic native Windows contract is:
+2 -1
View File
@@ -55,6 +55,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
configPath: process.env.THT_CONFIG ?? "config/tht.yaml",
dataRoot: config.dataRoot,
runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"),
secretRoots: config.workspaceRegistry.secretRoots,
secretsFile: config.secretsFile,
secretFiles: config.secretFiles,
});
@@ -141,7 +142,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
app.get("/internal/maintenance/status", async (req, reply) => {
return maintenanceBarrier.status();
});
sqlRoutes(app, { tht: tht as ThtRunner, getSettings });
sqlRoutes(app, { tht: tht as ThtRunner, getSettings, workspaceRegistry });
metaRoutes(app, { harnessDir: config.harnessDir, listModels });
workspaceRoutes(app, { registry: workspaceRegistry, config: config.workspaceRegistry, diagnose: workspaceDiagnoser });
settingsRoutes(app, { cfg: config, listModels, getSettings });
+31 -7
View File
@@ -2,7 +2,7 @@ import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams } from "node:ch
import type { AppConfig } from "../config.js";
import { RpcClient } from "../rpc/rpc-client.js";
import { SessionBridge } from "../bridge/session-bridge.js";
import type { ThtRunner } from "../tht/tht-runner.js";
import type { RuntimeConfigLease, ThtRunner } from "../tht/tht-runner.js";
import { buildPiChildEnv, canonicalPiProvider } from "./provider-credentials.js";
import { loadPiAuthProviders } from "./auth-providers.js";
import { secretValue } from "../config/secret-bundle.js";
@@ -14,6 +14,7 @@ export interface SessionRuntime {
bridge: SessionBridge;
child: ChildProcessWithoutNullStreams;
ownerKey?: string;
releaseRuntimeConfig?: () => void;
}
export interface RuntimeOptions {
@@ -24,6 +25,7 @@ export interface RuntimeOptions {
question?: string;
mode?: "new" | "resume";
principal?: PrincipalContext;
runtimeConfig?: RuntimeConfigLease;
}
/** Injectable child-process boundary; callbacks may ignore arguments in simpler tests. */
@@ -37,6 +39,7 @@ export class PiProcessManager {
private runtimes = new Map<string, SessionRuntime>();
private spawnFn: (
sessionId: string, author: string, provider: string | undefined, principal?: PrincipalContext,
runtimeConfigPath?: string,
) => ChildProcessWithoutNullStreams;
private loadAuthProviders: (agentDir: string) => ReadonlySet<string>;
@@ -47,16 +50,17 @@ export class PiProcessManager {
this.loadAuthProviders = opts?.authProviders
?? ((agentDir) => loadPiAuthProviders({ agentDir }));
if (opts?.spawnFn) {
this.spawnFn = (sessionId, author, provider, principal) =>
this.spawnPi(opts.spawnFn!, sessionId, author, provider, principal);
this.spawnFn = (sessionId, author, provider, principal, runtimeConfigPath) =>
this.spawnPi(opts.spawnFn!, sessionId, author, provider, principal, runtimeConfigPath);
} else {
this.spawnFn = (sessionId, author, provider, principal) =>
this.spawnPi(nodeSpawn, sessionId, author, provider, principal);
this.spawnFn = (sessionId, author, provider, principal, runtimeConfigPath) =>
this.spawnPi(nodeSpawn, sessionId, author, provider, principal, runtimeConfigPath);
}
}
private spawnPi(
spawnFn: SpawnFn, sessionId: string, author: string, provider: string | undefined, principal?: PrincipalContext,
spawnFn: SpawnFn, sessionId: string, author: string, provider: string | undefined,
principal?: PrincipalContext, runtimeConfigPath?: string,
): ChildProcessWithoutNullStreams {
// This is the final shared boundary for createFor(), spawnFor(), and resume(). Validate
// before auth-provider inspection, then make Pi consume the exact copied bytes rather than
@@ -94,6 +98,7 @@ export class PiProcessManager {
}
delete env.THT_DATA_ROOT;
if (this.cfg.dataRoot !== undefined) env.THT_DATA_ROOT = this.cfg.dataRoot;
if (runtimeConfigPath !== undefined) env.THT_CONFIG = runtimeConfigPath;
// pi 0.73 removed `--approve`: rpc mode is headless and its argv is intentionally minimal.
child = spawnFn(this.cfg.piBin, ["--mode", "rpc"], {
cwd: this.cfg.harnessDir,
@@ -132,15 +137,31 @@ export class PiProcessManager {
// SIGTERM to the in-flight Pi process and lose its pending gate.
const existing = this.runtimes.get(sessionId);
if (existing) {
o.runtimeConfig?.release();
throw new Error(`session runtime already active: ${sessionId}`);
}
if (o.principal) this.teardownForPrincipal(o.principal);
if (this.runtimes.size >= this.cfg.maxPiProcesses) {
o.runtimeConfig?.release();
throw new Error("max Pi processes reached");
}
const author = o.author ?? "dev@local";
const provider = canonicalPiProvider(o.provider ?? this.cfg.defaults.provider);
const child = this.spawnFn(sessionId, author, provider, o.principal);
let child: ChildProcessWithoutNullStreams;
try {
child = this.spawnFn(sessionId, author, provider, o.principal, o.runtimeConfig?.path);
} catch (error) {
o.runtimeConfig?.release();
throw error;
}
let runtimeConfigReleased = false;
const releaseRuntimeConfig = () => {
if (runtimeConfigReleased) return;
runtimeConfigReleased = true;
o.runtimeConfig?.release();
};
child.once("exit", releaseRuntimeConfig);
child.once("close", releaseRuntimeConfig);
let rt: SessionRuntime | undefined;
try {
const rpc = new RpcClient(child);
@@ -150,6 +171,7 @@ export class PiProcessManager {
bridge,
child,
ownerKey: o.principal ? `${o.principal.issuer}\0${o.principal.subject}` : undefined,
...(o.runtimeConfig ? { releaseRuntimeConfig } : {}),
};
rt = runtime;
bridge.beginTurn();
@@ -184,6 +206,7 @@ export class PiProcessManager {
return runtime;
} catch (error) {
if (rt && this.runtimes.get(sessionId) === rt) this.runtimes.delete(sessionId);
releaseRuntimeConfig();
try { child.kill(); } catch { /* preserve the initialization error */ }
throw error;
}
@@ -251,6 +274,7 @@ export class PiProcessManager {
// Delete before signalling the child so its asynchronous exit cannot be mistaken for a
// crash, and so a replacement installed by a later lifecycle operation is never targeted.
this.runtimes.delete(id);
expected.releaseRuntimeConfig?.();
expected.child.kill();
return true;
}
+19 -5
View File
@@ -77,6 +77,12 @@ export function sessionRoutes(
return typeof runner.withPrincipal === "function" ? runner.withPrincipal(principal) : runner;
};
const optionsWithRuntimeConfig = (runner: any, workspaceConfigPath: string | undefined, options: any) => (
workspaceConfigPath && typeof runner.acquireWorkspaceRuntime === "function"
? { ...options, runtimeConfig: runner.acquireWorkspaceRuntime(workspaceConfigPath) }
: options
);
const maintenanceReply = (reply: any) => reply.code(503).send({
code: "maintenance",
error: "Session admission is temporarily paused for maintenance. Try again shortly.",
@@ -413,9 +419,11 @@ export function sessionRoutes(
principal,
question: b.question,
};
let runtimeOptions = options;
let rt: ReturnType<PiProcessManager["createFor"]> | undefined;
try {
rt = d.mgr.createFor(id, options);
runtimeOptions = optionsWithRuntimeConfig(runner, workspaceConfigPath, options);
rt = d.mgr.createFor(id, runtimeOptions);
bindRuntime(id, rt, runner, workspaceConfigPath);
} catch (error) {
if (rt) d.mgr.teardownIfCurrent(id, rt);
@@ -430,9 +438,9 @@ export function sessionRoutes(
}
info(id, "Session created");
bootstrap(
id, rt, runner, workspaceConfigPath, d.mgr.configure(rt, options),
id, rt, runner, workspaceConfigPath, d.mgr.configure(rt, runtimeOptions),
runner.searchPack(b.question, id, workspaceConfigPath),
() => d.mgr.start(id, rt, options),
() => d.mgr.start(id, rt, runtimeOptions),
);
return { id };
} finally {
@@ -560,6 +568,7 @@ export function sessionRoutes(
principal,
mode: "resume" as const,
};
let runtimeOptions = options;
// Reopening is validation, not the transport commit point. Keep the old hub intact if
// persistence cannot be reopened.
@@ -589,7 +598,8 @@ export function sessionRoutes(
if (boundRuntimes.get(id) === current) boundRuntimes.delete(id);
d.mgr.teardownIfCurrent(id, current);
}
rt = d.mgr.createFor(id, options);
runtimeOptions = optionsWithRuntimeConfig(runner, workspaceConfigPath, options);
rt = d.mgr.createFor(id, runtimeOptions);
bindRuntime(id, rt, runner, workspaceConfigPath);
} catch {
// A created-but-unbound runtime is not usable. The old hub remains attached because
@@ -605,7 +615,11 @@ export function sessionRoutes(
// immediately before the first event produced by the new Resume.
d.hub.clear(id);
info(id, "Resuming session");
bootstrap(id, rt, runner, workspaceConfigPath, d.mgr.configure(rt, options), null, () => d.mgr.start(id, rt, options));
bootstrap(
id, rt, runner, workspaceConfigPath,
d.mgr.configure(rt, runtimeOptions), null,
() => d.mgr.start(id, rt, runtimeOptions),
);
return reply.code(200).send({ id, alreadyActive: false });
});
});
+40 -11
View File
@@ -3,22 +3,49 @@ import type { ThtRunner } from "../tht/tht-runner.js";
import { getPrincipal } from "../auth/auth.js";
import type { PrincipalContext } from "../auth/principal.js";
import type { Settings } from "../settings/settings-store.js";
import type { WorkspaceRegistry } from "../workspaces/registry.js";
export function sqlRoutes(app: FastifyInstance, deps: {
tht: ThtRunner; getSettings: (principal: PrincipalContext) => Promise<Settings>;
workspaceRegistry: WorkspaceRegistry;
}): void {
const runnerFor = (principal: PrincipalContext): any => {
const runner = deps.tht as any;
return typeof runner.withPrincipal === "function" ? runner.withPrincipal(principal) : runner;
};
const authorize = async (principal: PrincipalContext, id: string, workspace?: string) => {
try {
const runner = runnerFor(principal);
if (typeof runner.sessionShow !== "function") return {};
return await runner.sessionShow(id, workspace);
const isNotFound = (error: unknown) => /not found|non trovata|inesistente|404/i.test(
error instanceof Error ? error.message : String(error),
);
const locate = async (principal: PrincipalContext, id: string, legacyWorkspace?: string) => {
const runner = runnerFor(principal);
if (typeof runner.sessionShow !== "function") return { manifest: {}, workspace: legacyWorkspace };
const registry = deps.workspaceRegistry as Partial<WorkspaceRegistry>;
const revisions = typeof registry.listRetainedSnapshots === "function"
? await registry.listRetainedSnapshots.call(deps.workspaceRegistry)
: await deps.workspaceRegistry.list();
for (const revision of revisions) {
if (revision.state !== "operational") continue;
try {
const manifest = await runner.sessionShow(id, revision.snapshotPath);
if (!manifest) continue;
const saved = manifest as { workspace_id?: string; workspace_revision?: string };
if (saved.workspace_id && saved.workspace_revision) {
const pinned = await deps.workspaceRegistry.readPinned(saved.workspace_id, saved.workspace_revision);
return {
manifest,
workspace: pinned.workspaceConfigPath ?? (pinned as any).revision?.snapshotPath,
};
}
return { manifest, workspace: revision.snapshotPath };
} catch (error) {
if (!isNotFound(error)) throw error;
}
}
catch (error) {
if (/not found|non trovata|inesistente|404/i.test(error instanceof Error ? error.message : String(error))) return undefined;
try {
const manifest = await runner.sessionShow(id, legacyWorkspace);
return manifest ? { manifest, workspace: legacyWorkspace } : undefined;
} catch (error) {
if (isNotFound(error)) return undefined;
throw error;
}
};
@@ -30,8 +57,9 @@ export function sqlRoutes(app: FastifyInstance, deps: {
try {
principal = getPrincipal(req);
const settings = await deps.getSettings(principal);
workspace = settings.workspace;
if (!await authorize(principal, id, settings.workspace)) return reply.code(404).send({ error: "session not found" });
const located = await locate(principal, id, settings.workspace);
if (!located) return reply.code(404).send({ error: "session not found" });
workspace = located.workspace;
} catch {
return reply.code(503).send({ error: "session storage is unavailable" });
}
@@ -49,8 +77,9 @@ export function sqlRoutes(app: FastifyInstance, deps: {
try {
principal = getPrincipal(req);
const settings = await deps.getSettings(principal);
workspace = settings.workspace;
if (!await authorize(principal, id, settings.workspace)) return reply.code(404).send({ error: "session not found" });
const located = await locate(principal, id, settings.workspace);
if (!located) return reply.code(404).send({ error: "session not found" });
workspace = located.workspace;
} catch {
return reply.code(503).send({ error: "session storage is unavailable" });
}
+119 -3
View File
@@ -4,9 +4,13 @@ import {
closeSync, constants as fsConstants, existsSync, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync,
openSync, readFileSync, readSync, realpathSync, statSync, unlinkSync, writeFileSync,
} from "node:fs";
import { dirname, isAbsolute, join, relative } from "node:path";
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 } from "../workspaces/runtime-renderer.js";
import { parseWorkspaceYaml } from "../workspaces/schema.js";
export interface ThtConfig extends SecretBundleConfig {
thtBin: string;
@@ -14,6 +18,14 @@ export interface ThtConfig extends SecretBundleConfig {
configPath: string;
dataRoot?: string;
runtimeSnapshotRoot?: string;
secretRoots?: readonly string[];
}
export interface RuntimeConfigLease {
path: string;
workspaceId: string;
workspaceRevision: string;
release(): void;
}
export interface SessionRow {
@@ -87,18 +99,110 @@ export class ThtRunner {
return ["-c", this.cfg.configPath];
}
private assertWorkspaceSnapshot(path: string): void {
private assertWorkspaceSnapshot(path: string): { workspaceId: string; workspaceRevision: string } {
if (!this.cfg.runtimeSnapshotRoot) throw new Error("workspace snapshot root is not configured");
const snapshotsRoot = dirname(this.cfg.runtimeSnapshotRoot);
const pathRelative = relative(snapshotsRoot, path);
const match = /^([0-9a-f]{40})\/([a-z][a-z0-9-]{2,62})\.yaml$/.exec(pathRelative);
if (
pathRelative.startsWith("..") || isAbsolute(pathRelative)
|| !/^[0-9a-f]{40}\/[a-z][a-z0-9-]{2,62}\.yaml$/.test(pathRelative)
|| !match
) throw new Error("config path is not a trusted runtime snapshot");
const entry = lstatSync(path);
if (!entry.isFile() || entry.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<typeof parseWorkspaceYaml>;
workspaceId: string;
workspaceRevision: 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 = parseWorkspaceYaml(source);
if (workspace.workspace.id !== identity.workspaceId) {
throw new Error("workspace snapshot identity does not match its path");
}
return { workspace, ...identity };
} 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 <dataRoot>/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<string, unknown>;
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. */
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(),
);
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);
},
};
}
private runtimeSnapshotDirectory(): string {
@@ -228,6 +332,18 @@ export class ThtRunner {
run(
args: string[], workspaceConfigPath?: string, timeoutMs: number = ThtRunner.DEFAULT_TIMEOUT_MS,
): Promise<{ code: number; stdout: string; stderr: string }> {
if (
workspaceConfigPath && isAbsolute(workspaceConfigPath)
&& !this.runtimeSnapshots.has(workspaceConfigPath)
) {
let runtime: RuntimeConfigLease;
try {
runtime = this.acquireWorkspaceRuntime(workspaceConfigPath);
} catch (error) {
return Promise.reject(error);
}
return this.run(args, runtime.path, timeoutMs).finally(runtime.release);
}
return new Promise((resolve) => {
const env: NodeJS.ProcessEnv = { ...process.env };
delete env.THT_DATA_ROOT;
@@ -10,6 +10,16 @@ export interface RuntimePaths {
indexes: string;
}
export interface RuntimeIdentity {
workspaceId: string;
workspaceRevision: string;
}
export interface RuntimeInstallationOverlay {
session_storage?: unknown;
profile?: unknown;
}
function seconds(timeoutMs: number | undefined): number | undefined {
return timeoutMs === undefined ? undefined : Math.max(1, Math.ceil(timeoutMs / 1_000));
}
@@ -73,6 +83,8 @@ export function renderRuntimeConfig(
workspace: WorkspaceDescriptor,
bindings: RuntimeBindings,
paths: RuntimePaths,
identity?: RuntimeIdentity,
installation: RuntimeInstallationOverlay = {},
): string {
const canonical = validateCanonicalWorkspace(workspace);
if ([...bindings.dwh.missing, ...bindings.vector.missing, ...bindings.embedding.missing].length > 0) {
@@ -113,10 +125,20 @@ export function renderRuntimeConfig(
if (embeddingTimeout !== undefined) embedding.timeout = embeddingTimeout;
const rendered: Record<string, unknown> = {
...(identity ? {
runtime_identity: {
workspace_id: identity.workspaceId,
workspace_revision: identity.workspaceRevision,
},
} : {}),
...(installation.session_storage === undefined
? {} : { session_storage: installation.session_storage }),
...(installation.profile === undefined ? {} : { profile: installation.profile }),
language: canonical.workspace.language,
database,
vector_db: vectorDb,
embeddings: embedding,
roots: paths,
paths,
};
if (dwhDirect) {
+3 -2
View File
@@ -266,8 +266,9 @@ export function parseWorkspaceYaml(source: string): WorkspaceDescriptor {
const documents = parseAllDocuments(source, { uniqueKeys: true });
if (documents.length !== 1) throw new Error("Workspace YAML must contain exactly one document");
const document = documents[0];
if (document.errors.length > 0) {
throw new Error(`Invalid workspace YAML: ${document.errors.map((error) => error.message).join("; ")}`);
if (document.errors.length > 0 || document.warnings.length > 0) {
throw new Error(`Invalid workspace YAML: ${[...document.errors, ...document.warnings]
.map((error) => error.message).join("; ")}`);
}
return validateWorkspaceDescriptor(document.toJSON());
}
+29 -2
View File
@@ -1,10 +1,37 @@
import { test, expect } from "vitest";
import Fastify from "fastify";
import { authPreHandler, getPrincipal } from "../src/auth/auth.js";
import { chmodSync, mkdtempSync, rmSync, statSync } from "node:fs";
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expandLocalHome, localPrincipal } from "../src/auth/principal.js";
import { expandLocalHome, localPrincipal, upstreamPrincipal } from "../src/auth/principal.js";
test("server smoke trusted claims transform through nginx to a non-admin principal", () => {
const smoke = readFileSync("../scripts/unified-deployment-smoke.sh", "utf8");
const nginx = readFileSync("../docker/nginx.conf.template", "utf8");
const helper = smoke.match(/task13_server_auth_headers\(\) \{([\s\S]*?)\n\}/)?.[1] ?? "";
const trusted = Object.fromEntries(
[...helper.matchAll(/-H '([^:']+): ([^']+)'/g)].map((match) => [match[1].toLowerCase(), match[2]]),
);
const normalized: Record<string, string> = {};
for (const [header, suffix] of [
["x-thoth-principal-issuer", "principal_issuer"],
["x-thoth-principal-subject", "principal_subject"],
["x-thoth-principal-display-name", "principal_display_name"],
["x-thoth-is-admin", "is_admin"],
]) {
expect(nginx).toContain(`$http_x_thoth_trusted_${suffix}`);
const value = trusted[`x-thoth-trusted-${header.slice("x-thoth-".length)}`];
if (value !== undefined) normalized[header] = value;
}
expect(upstreamPrincipal(normalized)).toEqual({
issuer: "task13-proxy",
subject: "task13-user",
displayName: "Task 13 User",
isAdmin: false,
});
});
test("local mode resolves a stable local principal", async () => {
const app = Fastify();
+21
View File
@@ -168,6 +168,27 @@ function recordingChild() {
return ch;
}
test("Pi receives the leased workspace runtime config and releases it on direct teardown", () => {
const child = recordingChild();
let spawnEnv: NodeJS.ProcessEnv | undefined;
const release = vi.fn();
const mgr = new PiProcessManager(loadConfig({}), {
spawnFn: (_command, _args, options) => {
spawnEnv = options.env;
return child as any;
},
});
mgr.createFor("canonical-runtime", {
runtimeConfig: { path: "/trusted/runtime-uuid.yaml", release },
} as any);
expect(spawnEnv?.THT_CONFIG).toBe("/trusted/runtime-uuid.yaml");
// The child deliberately emits neither exit nor close. Ownership cleanup must not depend on it.
mgr.teardown("canonical-runtime");
expect(release).toHaveBeenCalledOnce();
});
test.each([
["new", "auth.json", '{"deepseek":{"key":"!runtime-auth-command runtime-secret /private/runtime-auth"}}\n'],
["new", "models.json", '{"providers":{"local-qwen":{"headers":["!runtime-model-command runtime-secret /private/runtime-model"]}}}\n'],
+15 -1
View File
@@ -601,6 +601,8 @@ test("session lifecycle locates a B session when installation default is A", asy
provider: "zai", model: "glm-5.2", thinking: "low",
};
const calls: string[] = [];
const runtimeSources: string[] = [];
const runtimeOptions: string[] = [];
let active: any;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
@@ -622,11 +624,21 @@ test("session lifecycle locates a B session when installation default is A", asy
calls.push(`reopen:${workspace}`);
expect(id).toBe("session-b");
},
acquireWorkspaceRuntime: (workspace: string) => {
runtimeSources.push(workspace);
return {
path: `/runtime/${runtimeSources.length}.yaml`,
workspaceId: "b-workspace",
workspaceRevision: "c".repeat(40),
release: vi.fn(),
};
},
} as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
mgr: {
get: () => active,
createFor: () => {
createFor: (_id: string, options: any) => {
runtimeOptions.push(options.runtimeConfig?.path ?? "missing");
active = { bridge: { onClientEvent: () => {}, respond: () => true, turnState: () => "idle" } };
return active;
},
@@ -673,6 +685,8 @@ test("session lifecycle locates a B session when installation default is A", asy
expect(calls).toContain(`list:${bPath}`);
expect(calls).toContain(`show:${bPath}`);
expect(calls).toContain(`reopen:${bPinnedPath}`);
expect(runtimeSources).toEqual([bPath, bPinnedPath]);
expect(runtimeOptions).toEqual(["/runtime/1.yaml", "/runtime/2.yaml"]);
});
test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+avvia", async () => {
+37
View File
@@ -85,6 +85,43 @@ test("POST /sessions/:id/sql/preview returns 500 when thtRunner throws", async (
expect(res.json()).toMatchObject({ error: /boom/ });
});
test("registry-backed SQL preview resolves and uses the session's pinned runtime revision", async () => {
const activePath = `/registry/snapshots/${"a".repeat(40)}/psd-clinical.yaml`;
const pinnedPath = `/registry/snapshots/${"b".repeat(40)}/psd-clinical.yaml`;
const calls: string[] = [];
const runner = {
sessionShow: async (_id: string, workspace: string) => {
calls.push(`show:${workspace}`);
if (workspace === activePath) return {
id: "s1", workspace_id: "psd-clinical", workspace_revision: "b".repeat(40),
};
throw new Error("session not found");
},
sqlPreview: async (_id: string, _page: unknown, workspace: string) => {
calls.push(`preview:${workspace}`);
return { columns: [], rows: [], execution_ms: 0, truncated: false };
},
};
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: { ...runner, withPrincipal: () => runner } as any,
getSettings: () => ({ workspace: "legacy-default" }) as any,
workspaceRegistry: {
list: async () => [{
id: "psd-clinical", commit: "a".repeat(40), blob: "c".repeat(40),
snapshotPath: activePath, state: "operational",
}],
readPinned: async () => ({ workspace: {}, workspaceConfigPath: pinnedPath }),
} as any,
});
const response = await app.inject({
method: "POST", url: "/sessions/s1/sql/preview", payload: { limit: 10 },
});
expect(response.statusCode).toBe(200);
expect(calls).toEqual([`show:${activePath}`, `preview:${pinnedPath}`]);
});
// Workspace registry route coverage lives in routes-workspaces.test.ts. `/workspaces` no longer
// reads legacy harness files: the Git registry is the single shared source of truth.
@@ -0,0 +1,167 @@
import { execFile } from "node:child_process";
import { chmodSync, existsSync, mkdtempSync, mkdirSync, 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 { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { ThtRunner } from "../src/tht/tht-runner.js";
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
const runFile = promisify(execFile);
const harnessDir = resolve("../harness");
const thtBin = join(harnessDir, ".venv", "bin", "tht");
const roots: string[] = [];
const canonicalWorkspace = `workspace:
schema_version: 2
id: psd-clinical
name: Runtime handoff
language: en
dwh:
engine: postgres
database: analytics
schema: mart
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: pgvector
database: analytics
schema: vectors
collection: documents
dimensions: 768
distance: cosine
supported_transports: [pgvector_direct]
embedding:
provider: ollama_compatible
model: embed
dimensions: 768
llm_policy:
allowed: [zai/glm-5.2]
`;
afterEach(() => {
vi.unstubAllEnvs();
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
});
async function git(cwd: string, args: string[]): Promise<string> {
return (await runFile("git", args, { cwd })).stdout.trim();
}
async function fixture() {
const root = mkdtempSync(join(tmpdir(), "tht-runtime-handoff-"));
roots.push(root);
const remote = join(root, "remote.git");
const source = join(root, "source");
const registryRoot = join(root, "registry");
const secretRoot = join(root, "secrets");
const dataRoot = join(root, "data");
await git(root, ["init", "--bare", "--initial-branch=main", remote]);
mkdirSync(source);
await git(source, ["init", "--initial-branch=main"]);
await git(source, ["config", "user.name", "Runtime Handoff Test"]);
await git(source, ["config", "user.email", "runtime-handoff@example.invalid"]);
mkdirSync(join(source, "workspaces"));
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), canonicalWorkspace);
await git(source, ["add", "workspaces/psd-clinical.yaml"]);
await git(source, ["commit", "-m", "Canonical workspace"]);
await git(source, ["remote", "add", "origin", remote]);
await git(source, ["push", "origin", "main"]);
mkdirSync(secretRoot);
for (const name of ["dwh-password", "vector-password"]) {
const path = join(secretRoot, name);
writeFileSync(path, `${name}-value`, { mode: 0o600 });
chmodSync(path, 0o600);
}
mkdirSync(dataRoot);
const registryConfig: WorkspaceRegistryConfig = {
root: registryRoot,
remoteUrl: remote,
branch: "main",
gitAuthorName: "Runtime Handoff Test",
gitAuthorEmail: "runtime-handoff@example.invalid",
installationId: "test",
secretRoots: [secretRoot],
maxImportBytes: 1024 * 1024,
maxImportEntries: 16,
};
const registry = new WorkspaceRegistry(registryConfig);
await registry.bootstrap();
const revision = (await registry.list())[0];
const environment = {
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct",
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.invalid",
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
THT_WS_PSD_CLINICAL_DWH_USER: "reader",
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: join(secretRoot, "dwh-password"),
THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "pgvector_direct",
THT_WS_PSD_CLINICAL_VECTOR_HOST: "vector.invalid",
THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432",
THT_WS_PSD_CLINICAL_VECTOR_USER: "vector-reader",
THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: join(secretRoot, "vector-password"),
THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "http://embedding.invalid",
};
for (const [name, value] of Object.entries(environment)) vi.stubEnv(name, value);
vi.stubEnv("THT_HOME", join(root, "home"));
return { root, dataRoot, registry, registryConfig, revision };
}
function runnerFor(f: Awaited<ReturnType<typeof fixture>>): ThtRunner {
return new ThtRunner({
thtBin,
harnessDir,
configPath: "config/tht.yaml",
dataRoot: f.dataRoot,
runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"),
secretRoots: f.registryConfig.secretRoots,
} as any);
}
test("real schema-v2 registry revision loads through ThtRunner and the harness contract", async () => {
const f = await fixture();
const runner = runnerFor(f);
expect(await runner.sessionList(f.revision.snapshotPath)).toEqual([]);
const created = await runner.sessionNew({
question: "runtime handoff",
workspaceConfigPath: f.revision.snapshotPath,
workspaceId: f.revision.id,
workspaceRevision: f.revision.commit,
});
expect(await runner.sessionShow(created.id, f.revision.snapshotPath)).toMatchObject({
id: created.id,
workspace_id: "psd-clinical",
workspace_revision: f.revision.commit,
});
expect(existsSync(join(
f.dataRoot, "sessions", "psd-clinical", "sessions", created.id, "session_manifest.yaml",
))).toBe(true);
expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]);
});
test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => {
const f = await fixture();
const app = buildApp(loadConfig({
AUTH_MODE: "none",
THT_HARNESS_DIR: harnessDir,
THT_BIN: thtBin,
THT_DATA_ROOT: f.dataRoot,
THT_WORKSPACE_REGISTRY_ROOT: f.registryConfig.root,
THT_WORKSPACE_GIT_REMOTE: f.registryConfig.remoteUrl,
THT_WORKSPACE_SECRET_ROOTS: f.registryConfig.secretRoots.join(","),
}), {
thtRunner: runnerFor(f),
workspaceRegistry: f.registry,
mgr: { get: () => undefined } as any,
});
try {
const response = await app.inject({ method: "GET", url: "/sessions?scope=mine" });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual([]);
} finally {
await app.close();
}
});
+7
View File
@@ -61,6 +61,13 @@ test("rejects unknown keys and invalid immutable IDs", () => {
.toThrow(/id/i);
});
test("rejects executable or otherwise custom YAML tags in canonical descriptors", () => {
expect(() => parseWorkspaceYaml(validYaml.replace(
"name: Policlinico San Donato",
"name: !command echo-never-execute",
))).toThrow(/tag|yaml/i);
});
test("accepts optional connection ports and timeouts but rejects unsafe values", () => {
expect(parseWorkspaceYaml(validYaml).dwh.port).toBe(5432);
expect(() => parseWorkspaceYaml(validYaml.replace("port: 5432", "port: 0")))
+60
View File
@@ -6,6 +6,7 @@ from tht.config import (
PostgresDwhConfig,
ThothRestDwhConfig,
ThothVectorHttpConfig,
workspace_id_for_config,
load_config,
)
from tht.adapters.evidence import FilesystemEvidenceSource, HttpManifestEvidenceSource
@@ -86,6 +87,65 @@ roots:
assert cfg.roots.sessions.as_posix() == "build/sessions"
def test_runtime_handoff_preserves_canonical_identity_and_durable_roots(monkeypatch, tmp_path):
data_root = tmp_path / "data"
runtime_root = data_root / "sessions" / "psd-clinical"
workspace = tmp_path / "runtime-random-uuid.yaml"
workspace.write_text(f"""
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
vectors:
type: pgvector_direct
connection: {{database: analytics, schema: vectors, user: vector, password: secret}}
roots:
sessions: {runtime_root / 'sessions'}
artifacts: {runtime_root / 'artifacts'}
indexes: {runtime_root / 'indexes'}
embeddings: {{base_url: http://embedding.invalid, model: embed, dim: 768}}
""")
monkeypatch.setenv("THT_DATA_ROOT", str(data_root))
cfg = load_config(workspace)
assert cfg._workspace_id == "psd-clinical"
assert cfg._workspace_revision == "a" * 40
assert cfg.paths.sessions == runtime_root / "sessions"
assert cfg.paths.artifacts == runtime_root / "artifacts"
assert cfg.paths.indexes == runtime_root / "indexes"
def test_runtime_identity_is_authoritative_over_runtime_filename(tmp_path):
workspace = tmp_path / "runtime-random-uuid.yaml"
workspace.write_text(f"""
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
roots: {{sessions: sessions, artifacts: artifacts, indexes: indexes}}
""")
cfg = load_config(workspace)
assert workspace_id_for_config(cfg, workspace) == "psd-clinical"
def test_load_config_rejects_executable_yaml_tags_without_running_them(tmp_path):
marker = tmp_path / "must-not-exist"
workspace = tmp_path / "workspace.yaml"
workspace.write_text(f"dwh: !command touch {marker}\n")
with pytest.raises(ConfigError, match="YAML|configurazione"):
load_config(workspace)
assert not marker.exists()
def test_loads_direct_discriminated_resources(tmp_path):
workspace = tmp_path / "workspace.yaml"
workspace.write_text(
+3 -3
View File
@@ -5,7 +5,7 @@ import typer
from tht.cli.config_cmd import CONFIG_OPT
from tht.cli.schema_cmd import _load_config_or_exit
from tht.config import workspace_id_from_path
from tht.config import workspace_id_for_config
KIND_MAP = {
"evidence": ["evidence"],
@@ -60,7 +60,7 @@ def search_cmd(
cfg = _load_config_or_exit(config)
from tht.search.evidence import validate_corpus_workspace
workspace_id = workspace_id_from_path(config)
workspace_id = workspace_id_for_config(cfg, config)
validate_corpus_workspace(cfg, workspace_id)
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
require_vector_cfg(cfg)
@@ -264,7 +264,7 @@ def pack_cmd(
cfg = _load_config_or_exit(config)
from tht.search.evidence import validate_corpus_workspace
workspace_id = workspace_id_from_path(config)
workspace_id = workspace_id_for_config(cfg, config)
validate_corpus_workspace(cfg, workspace_id)
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
require_vector_cfg(cfg)
+24 -2
View File
@@ -162,6 +162,11 @@ class PathsConfig(BaseModel):
sessions: Path = Path("sessions")
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}$")
class WorkspaceRoots(PathsConfig):
pass
@@ -305,7 +310,9 @@ class ExecutionConfig(BaseModel):
class Config(BaseModel):
_workspace_id: str = PrivateAttr(default="default")
_workspace_revision: str | None = PrivateAttr(default=None)
_config_source: str = PrivateAttr(default="direct")
runtime_identity: RuntimeIdentityConfig | None = None
dwh: DwhResourceConfig
vectors: VectorResourceConfig | None = None
session_storage: SessionStorageConfig | None = None
@@ -354,10 +361,20 @@ def workspace_id_from_path(path: Path) -> str:
return path.resolve().stem.lower().replace(".", "-").replace("_", "-")
def workspace_id_for_config(config: Config, path: Path) -> str:
"""Return the canonical runtime identity, falling back for legacy configs."""
if config.runtime_identity is not None:
return config.runtime_identity.workspace_id
return workspace_id_from_path(path)
def load_config(path: Path) -> Config:
if not path.exists():
raise ConfigError(f"File di configurazione non trovato: {path}")
raw = yaml.safe_load(path.read_text())
try:
raw = yaml.safe_load(path.read_text())
except yaml.YAMLError as exc:
raise ConfigError(f"Configurazione YAML non valida: {path}") from exc
if not isinstance(raw, dict):
raise ConfigError(f"Configurazione non valida (atteso un mapping YAML): {path}")
expanded = _resolve_secret_files(_expand_env(raw))
@@ -407,7 +424,12 @@ def load_config(path: Path) -> Config:
FutureWarning,
stacklevel=2,
)
cfg._workspace_id = workspace_id_from_path(path)
cfg._workspace_id = workspace_id_for_config(cfg, path)
cfg._workspace_revision = (
cfg.runtime_identity.workspace_revision
if cfg.runtime_identity is not None
else None
)
cfg._config_source = path.resolve().as_posix()
return cfg
+20 -10
View File
@@ -23,12 +23,12 @@ const expected = {
THT_WS_TASK13_SMOKE_DWH_HOST: "dwh.task13.invalid",
THT_WS_TASK13_SMOKE_DWH_PORT: "5432",
THT_WS_TASK13_SMOKE_DWH_USER: "task13_reader",
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: "/run/secrets/thothii.secrets",
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: "/run/secrets/task13-runtime-password",
THT_WS_TASK13_SMOKE_VECTOR_TRANSPORT: "pgvector_direct",
THT_WS_TASK13_SMOKE_VECTOR_HOST: "vector.task13.invalid",
THT_WS_TASK13_SMOKE_VECTOR_PORT: "5432",
THT_WS_TASK13_SMOKE_VECTOR_USER: "task13_vector_reader",
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: "/run/secrets/thothii.secrets",
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: "/run/secrets/task13-runtime-password",
THT_WS_TASK13_SMOKE_EMBEDDING_BASE_URL: profile === "local"
? `http://${config.name}-llm:9000`
: "https://embedding.task13.invalid",
@@ -55,6 +55,14 @@ if (coreBundle.length !== 1) throw new Error("core lacks exactly one runtime sec
if ((frontend.secrets || []).length !== 0) throw new Error("frontend received a runtime secret");
const mounts = core.volumes || [];
const runtimePasswordMounts = mounts.filter(
(mount: any) => mount.target === "/run/secrets/task13-runtime-password",
);
if (runtimePasswordMounts.length !== 1 || runtimePasswordMounts[0].type !== "bind"
|| !runtimePasswordMounts[0].read_only || !statSync(runtimePasswordMounts[0].source).isFile()) {
throw new Error("runtime fixture lacks one readable, read-only password-file bind");
}
accessSync(runtimePasswordMounts[0].source, constants.R_OK);
for (const target of [
"/home/thoth/.pi/agent/auth.json",
"/home/thoth/.pi/agent/models.json",
@@ -73,9 +81,9 @@ for (const target of [
}
const resolverEnvironment = { ...core.environment };
resolverEnvironment.THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE = bundleSource;
resolverEnvironment.THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE = bundleSource;
const bindings = resolveRuntimeBindings(workspace, resolverEnvironment, [dirname(bundleSource)]);
resolverEnvironment.THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE = runtimePasswordMounts[0].source;
resolverEnvironment.THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE = runtimePasswordMounts[0].source;
const bindings = resolveRuntimeBindings(workspace, resolverEnvironment, [dirname(runtimePasswordMounts[0].source)]);
for (const [role, binding] of Object.entries(bindings)) {
if ((binding as any).missing.length !== 0) {
throw new Error(`workspace resolver reports missing ${role} bindings: ${(binding as any).missing.join(",")}`);
@@ -88,18 +96,20 @@ const runtime = parse(renderRuntimeConfig(workspace, bindings, {
}));
if (runtime.database.host !== expected.THT_WS_TASK13_SMOKE_DWH_HOST
|| runtime.database.user !== expected.THT_WS_TASK13_SMOKE_DWH_USER
|| runtime.database.password_file !== bundleSource) {
|| runtime.database.password_file !== runtimePasswordMounts[0].source) {
throw new Error("workspace resolver produced the wrong DWH runtime");
}
if (runtime.vector_db.host !== expected.THT_WS_TASK13_SMOKE_VECTOR_HOST
|| runtime.vector_db.user !== expected.THT_WS_TASK13_SMOKE_VECTOR_USER
|| runtime.vector_db.password_file !== bundleSource) {
|| runtime.vector_db.password_file !== runtimePasswordMounts[0].source) {
throw new Error("workspace resolver produced the wrong vector runtime");
}
if (runtime.embeddings.base_url !== expected.THT_WS_TASK13_SMOKE_EMBEDDING_BASE_URL) {
throw new Error("workspace resolver produced the wrong embedding runtime");
}
const secret = readFileSync(bundleSource, "utf8").trim();
if (JSON.stringify(config).includes(secret) || JSON.stringify(runtime).includes(secret)) {
throw new Error("fixture render or resolver output leaked secret content");
}
const runtimePassword = readFileSync(runtimePasswordMounts[0].source, "utf8");
if (JSON.stringify(config).includes(secret)) throw new Error("fixture render leaked application bundle content");
if (JSON.stringify(runtime).includes(secret)) throw new Error("runtime render leaked application bundle content");
if (JSON.stringify(config).includes(runtimePassword)) throw new Error("fixture render leaked runtime password content");
if (JSON.stringify(runtime).includes(runtimePassword)) throw new Error("runtime render leaked runtime password content");
+3 -2
View File
@@ -30,6 +30,7 @@ TASK13_LOG="$fixture/task13.log"
TASK13_INSTALLATION="$fixture/thothii-installation.yaml"
TASK13_PI_AUTH="$fixture/pi-auth.json"
TASK13_SECRETS="$fixture/thothii.secrets"
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/runtime-password"
TASK13_PI_MODELS="$fixture/models.json"
TASK13_PI_SETTINGS="$fixture/settings.json"
TASK13_LLM_SERVER="$fixture/fake-llm.mjs"
@@ -79,8 +80,8 @@ else
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/session-runtime-password"
TASK13_SESSION_MIGRATOR_PASSWORD_FILE="$fixture/session-migrator-password"
TASK13_SESSION_CA="$fixture/session-ca.pem"
TASK13_SESSION_PASSWORD="task13-runtime-$profile"
TASK13_SESSION_MIGRATOR_PASSWORD="task13-migrator-$profile"
TASK13_SESSION_PASSWORD="fixture-private-token-$profile"
TASK13_SESSION_MIGRATOR_PASSWORD="fixture-migrator-token-$profile"
task13_write_server_fixture_files
compose_files=(
-f "$root/compose.yaml"
+15 -8
View File
@@ -186,8 +186,9 @@ task13_write_environment() {
task13_write_fixture_files() {
printf '{}\n' >"$TASK13_PI_AUTH"
printf 'THT_MODEL_API_KEY=%s\n' "$TASK13_SECRET_VALUE" >"$TASK13_SECRETS"
printf '%s' "task13-runtime-password-$TASK13_RUN_ID" >"$TASK13_SESSION_RUNTIME_PASSWORD"
chmod 0644 "$TASK13_PI_AUTH"
chmod 0600 "$TASK13_SECRETS"
chmod 0600 "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD"
cat >"$TASK13_PI_MODELS" <<EOF
{
@@ -283,12 +284,12 @@ services:
THT_WS_TASK13_SMOKE_DWH_HOST: dwh.task13.invalid
THT_WS_TASK13_SMOKE_DWH_PORT: "5432"
THT_WS_TASK13_SMOKE_DWH_USER: task13_reader
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: /run/secrets/thothii.secrets
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: /run/secrets/task13-runtime-password
THT_WS_TASK13_SMOKE_VECTOR_TRANSPORT: pgvector_direct
THT_WS_TASK13_SMOKE_VECTOR_HOST: vector.task13.invalid
THT_WS_TASK13_SMOKE_VECTOR_PORT: "5432"
THT_WS_TASK13_SMOKE_VECTOR_USER: task13_vector_reader
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: /run/secrets/thothii.secrets
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: /run/secrets/task13-runtime-password
THT_WS_TASK13_SMOKE_EMBEDDING_BASE_URL: http://$TASK13_LLM_CONTAINER:9000
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
@@ -300,6 +301,7 @@ services:
- $TASK13_PI_SETTINGS:/home/thoth/.pi/agent/settings.json:ro
- workspace-registry:/data/workspace-registry
- sessions:/data/sessions
- $TASK13_SESSION_RUNTIME_PASSWORD:/run/secrets/task13-runtime-password:ro
- $TASK13_REMOTE:/fixtures/remote.git:ro
frontend:
image: $TASK13_FRONTEND_IMAGE
@@ -342,8 +344,8 @@ task13_write_server_fixture_files() {
local data_root pi_root registry_root remote_path workspace_path
printf '{}\n' >"$TASK13_PI_AUTH"
printf 'THT_MODEL_API_KEY=%s\n' "$TASK13_SECRET_VALUE" >"$TASK13_SECRETS"
printf '%s\n' "$TASK13_SESSION_PASSWORD" >"$TASK13_SESSION_RUNTIME_PASSWORD"
printf '%s\n' "$TASK13_SESSION_MIGRATOR_PASSWORD" >"$TASK13_SESSION_MIGRATOR_PASSWORD_FILE"
printf '%s' "$TASK13_SESSION_PASSWORD" >"$TASK13_SESSION_RUNTIME_PASSWORD"
printf '%s' "$TASK13_SESSION_MIGRATOR_PASSWORD" >"$TASK13_SESSION_MIGRATOR_PASSWORD_FILE"
cat >"$TASK13_SESSION_CA" <<'EOF'
-----BEGIN CERTIFICATE-----
VEFTSzEzLURJU1BPU0FCTEUtU0VTU0lPTi1DQQ==
@@ -396,17 +398,18 @@ services:
THT_WS_TASK13_SMOKE_DWH_HOST: dwh.task13.invalid
THT_WS_TASK13_SMOKE_DWH_PORT: "5432"
THT_WS_TASK13_SMOKE_DWH_USER: task13_reader
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: /run/secrets/thothii.secrets
THT_WS_TASK13_SMOKE_DWH_PASSWORD_FILE: /run/secrets/task13-runtime-password
THT_WS_TASK13_SMOKE_VECTOR_TRANSPORT: pgvector_direct
THT_WS_TASK13_SMOKE_VECTOR_HOST: vector.task13.invalid
THT_WS_TASK13_SMOKE_VECTOR_PORT: "5432"
THT_WS_TASK13_SMOKE_VECTOR_USER: task13_vector_reader
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: /run/secrets/thothii.secrets
THT_WS_TASK13_SMOKE_VECTOR_PASSWORD_FILE: /run/secrets/task13-runtime-password
THT_WS_TASK13_SMOKE_EMBEDDING_BASE_URL: https://embedding.task13.invalid
labels:
io.thothii.task13.run: "$TASK13_RUN_ID"
volumes:
- $remote_path:/fixtures/remote.git:ro
- $TASK13_SESSION_RUNTIME_PASSWORD:/run/secrets/task13-runtime-password:ro
frontend:
image: $TASK13_FRONTEND_IMAGE
build:
@@ -614,6 +617,7 @@ task13_server_auth_headers() {
-H 'x-thoth-trusted-principal-issuer: task13-proxy'
-H 'x-thoth-trusted-principal-subject: task13-user'
-H 'x-thoth-trusted-principal-display-name: Task 13 User'
-H 'x-thoth-trusted-is-admin: 0'
)
}
@@ -1325,10 +1329,13 @@ task13_self_test_server_auth_hop_contract() {
for header in \
x-thoth-trusted-principal-issuer \
x-thoth-trusted-principal-subject \
x-thoth-trusted-principal-display-name; do
x-thoth-trusted-principal-display-name \
x-thoth-trusted-is-admin; do
[[ "$joined" == *"$header:"* ]] \
|| task13_fail "server smoke omits trusted frontend hop header: $header"
done
[[ "$joined" == *'x-thoth-trusted-is-admin: 0'* ]] \
|| task13_fail "server smoke admin claim is not the exact non-admin value"
[[ "$joined" != *'x-thoth-principal-issuer:'* ]] \
|| task13_fail "server smoke sends public identity headers to the frontend hop"
}