refactor: share registry runtime configuration leases
This commit is contained in:
+36
-108
@@ -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<string, RuntimeSnapshot>();
|
||||
private readonly runtimeLeases = new Map<string, RuntimeConfigLease>();
|
||||
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 <config>` 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<typeof parseWorkspaceYaml>;
|
||||
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 <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. */
|
||||
/** 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 };
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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<typeof lstatSync> {
|
||||
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<string, PublishedIdentity>();
|
||||
|
||||
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<string, unknown>): 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }); }
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user