fix: complete durable workspace runtime config handoff
This commit is contained in:
@@ -322,29 +322,40 @@ export class ThtRunner {
|
||||
}
|
||||
let snapshotFd: number | undefined;
|
||||
let canonicalFd: number | undefined;
|
||||
let manifestFd: number | undefined;
|
||||
let ch;
|
||||
try {
|
||||
snapshotFd = workspaceConfigPath && this.runtimeSnapshots.has(workspaceConfigPath)
|
||||
? this.openTrustedRuntimeSnapshot(workspaceConfigPath) : undefined;
|
||||
// Runtime lease publication is durable, but the child must consume the verified
|
||||
// bytes rather than reopening a mutable pathname after spawn. Keep canonical -c
|
||||
// for CLI compatibility and hand the same open file as fd 3.
|
||||
canonicalFd = snapshotFd === undefined && workspaceConfigPath && this.runtimeLeases.has(workspaceConfigPath)
|
||||
? openSync(workspaceConfigPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW) : undefined;
|
||||
const lease = workspaceConfigPath ? this.runtimeLeases.get(workspaceConfigPath) : undefined;
|
||||
// Registry leases retain the verified config bytes in fd 3 and the separately
|
||||
// published manifest in fd 4. The argv remains the canonical -c pathname for
|
||||
// diagnostics/compatibility; the harness never trusts that pathname for bytes.
|
||||
canonicalFd = snapshotFd === undefined && lease
|
||||
? openSync(lease.path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW) : undefined;
|
||||
manifestFd = lease
|
||||
? openSync(lease.manifestPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW) : undefined;
|
||||
if (lease) {
|
||||
env.THT_CONFIG_FD = "3";
|
||||
env.THT_CONFIG_MANIFEST_FD = "4";
|
||||
env.THT_CONFIG_MANIFEST_SHA256 = lease.manifestSha256;
|
||||
}
|
||||
const handoffFd = snapshotFd ?? canonicalFd;
|
||||
if (canonicalFd !== undefined) env.THT_CONFIG_FD = "3";
|
||||
ch = spawn(
|
||||
this.cfg.thtBin,
|
||||
snapshotFd === undefined ? this.buildArgv(args, workspaceConfigPath) : [...args, "-c", "/dev/fd/3"],
|
||||
{
|
||||
cwd: this.cfg.harnessDir,
|
||||
env,
|
||||
...(handoffFd === undefined ? {} : { stdio: ["ignore", "pipe", "pipe", handoffFd] }),
|
||||
...(handoffFd === undefined ? {} : {
|
||||
stdio: ["ignore", "pipe", "pipe", handoffFd, ...(manifestFd === undefined ? [] : [manifestFd])],
|
||||
}),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (snapshotFd !== undefined) closeSync(snapshotFd);
|
||||
if (canonicalFd !== undefined) closeSync(canonicalFd);
|
||||
if (manifestFd !== undefined) closeSync(manifestFd);
|
||||
}
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
@@ -3,8 +3,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { isIP } from "node:net";
|
||||
import { domainToASCII } from "node:url";
|
||||
import {
|
||||
closeSync, constants as fsConstants, fstatSync, lstatSync,
|
||||
existsSync, mkdirSync, openSync, readFileSync,
|
||||
existsSync, lstatSync, readFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { parseAllDocuments } from "yaml";
|
||||
@@ -20,6 +19,8 @@ import { parseWorkspaceYaml, validateOperationalWorkspace, type WorkspaceDescrip
|
||||
export interface RuntimeConfigLease {
|
||||
path: string;
|
||||
manifestPath: string;
|
||||
/** SHA-256 of the exact durable manifest bytes handed to the child. */
|
||||
manifestSha256: string;
|
||||
workspaceId: string;
|
||||
workspaceRevision: string;
|
||||
release(): void;
|
||||
@@ -53,12 +54,13 @@ interface SnapshotIdentity {
|
||||
interface PublishedIdentity {
|
||||
path: string;
|
||||
manifestPath: string;
|
||||
/** SHA-256 of the exact durable manifest bytes handed to the child. */
|
||||
manifestSha256: string;
|
||||
workspaceId: string;
|
||||
workspaceRevision: string;
|
||||
digest: string;
|
||||
content: string;
|
||||
manifest: string;
|
||||
refs: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,11 +121,12 @@ 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");
|
||||
if (!existsSync(input.runtimeSnapshotRoot)) mkdirSync(input.runtimeSnapshotRoot, { recursive: true, mode: 0o700 });
|
||||
// Registry snapshots are produced by WorkspaceRegistry. Never recursively
|
||||
// create this security boundary from a pathname (an ancestor could be swapped).
|
||||
if (!existsSync(input.runtimeSnapshotRoot)) throw new Error("runtime snapshot root is unavailable");
|
||||
this.assertDirectory(input.runtimeSnapshotRoot, "runtime snapshot root");
|
||||
this.env = { ...(input.env ?? process.env) };
|
||||
this.secretRoots = [...(input.secretRoots ?? [])];
|
||||
@@ -149,7 +152,7 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
const renderedDigest = digest(rendered);
|
||||
const base = {
|
||||
workspace_id: snapshot.workspaceId, workspace_revision: snapshot.workspaceRevision,
|
||||
descriptor_git_blob: snapshot.descriptorBlob ?? "unknown",
|
||||
descriptor_git_blob: snapshot.descriptorBlob!,
|
||||
descriptor_sha256: snapshot.digest,
|
||||
config_sha256: renderedDigest,
|
||||
config_dwh_binding: this.computeBinding(rendered),
|
||||
@@ -158,9 +161,8 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
const identity: PublishedIdentity = {
|
||||
path: result.path, manifestPath: result.manifestPath, workspaceId: snapshot.workspaceId,
|
||||
workspaceRevision: snapshot.workspaceRevision, digest: renderedDigest, content: rendered,
|
||||
manifest: result.manifest, refs: 1,
|
||||
manifest: result.manifest, manifestSha256: result.manifest_sha256,
|
||||
};
|
||||
this.published.set(identity.path, identity);
|
||||
return this.lease(identity);
|
||||
}
|
||||
|
||||
@@ -194,14 +196,14 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
return { workspace_id: "unknown", config_fingerprint: `sha256:${digest(content)}`, input_fingerprint: `sha256:${digest(content)}` };
|
||||
}
|
||||
|
||||
private publishSecure(workspaceId: string, revision: string, content: string, manifestBase: Record<string, unknown>): {path:string; manifestPath:string; manifest:string} {
|
||||
private publishSecure(workspaceId: string, revision: string, content: string, manifestBase: Record<string, unknown>): {path:string; manifestPath:string; manifest:string; manifest_sha256:string} {
|
||||
return this.helper("publish", { data_root: this.input.dataRoot, workspace_id: workspaceId,
|
||||
workspace_revision: revision, config_hex: Buffer.from(content).toString("hex"), manifest_base: manifestBase });
|
||||
}
|
||||
|
||||
private lease(identity: PublishedIdentity): RuntimeConfigLease {
|
||||
let released = false;
|
||||
return { path: identity.path, manifestPath: identity.manifestPath, workspaceId: identity.workspaceId, workspaceRevision: identity.workspaceRevision,
|
||||
return { path: identity.path, manifestPath: identity.manifestPath, manifestSha256: identity.manifestSha256, workspaceId: identity.workspaceId, workspaceRevision: identity.workspaceRevision,
|
||||
release: () => { if (released) return; released = true; /* Durable revision-owned state: release only drops our local handle/ref. */ }, };
|
||||
}
|
||||
|
||||
@@ -217,31 +219,30 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
|
||||
private readSnapshot(path: string): SnapshotIdentity {
|
||||
if (!isAbsolute(path)) throw new Error("workspace snapshot path must be absolute");
|
||||
const rel = relative(this.input.runtimeSnapshotRoot, path);
|
||||
const root = resolve(this.input.runtimeSnapshotRoot);
|
||||
const rel = relative(root, path);
|
||||
const match = /^([0-9a-f]{40})\/([a-z][a-z0-9-]{2,62})\.yaml$/.exec(rel);
|
||||
if (!match || rel.startsWith("..") || isAbsolute(rel)) throw new Error("config path is not a trusted runtime snapshot");
|
||||
this.assertDirectory(join(this.input.runtimeSnapshotRoot, match[1]), "workspace snapshot parent");
|
||||
const fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
||||
try {
|
||||
const before = fstatSync(fd);
|
||||
if (!before.isFile() || before.nlink !== 1 || (before.mode & 0o077) !== 0) 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");
|
||||
let descriptorBlob: string | undefined;
|
||||
const repositoryRoot = join(dirname(this.input.runtimeSnapshotRoot), "repo");
|
||||
const verified = this.helper("verified-snapshot", { snapshots_root: this.input.runtimeSnapshotRoot, ...(existsSync(repositoryRoot) ? { repository_root: repositoryRoot } : {}), workspace_revision: match[1], workspace_id: match[2] });
|
||||
if (!verified || verified.sha256 !== digest(source) || verified.source !== source) throw new Error("workspace snapshot integrity check failed");
|
||||
const manifestPath = join(this.input.runtimeSnapshotRoot, match[1], "snapshot.json");
|
||||
if (!existsSync(manifestPath)) throw new Error("workspace snapshot integrity check failed");
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as any;
|
||||
const record = Array.isArray(manifest.revisions) ? manifest.revisions.find((r: any) => r?.id === match[2]) : undefined;
|
||||
const expectedFile = `${match[2]}.yaml`;
|
||||
if (manifest.head !== match[1] || !record || record.commit !== match[1] || record.snapshotPath !== path || typeof record.blob !== "string" || manifest.files?.[expectedFile] !== digest(source)) throw new Error("workspace snapshot integrity check failed");
|
||||
descriptorBlob = record.blob;
|
||||
return { workspace, workspaceId: match[2], workspaceRevision: match[1], revisionContentRoot: dirname(path), digest: digest(source), descriptorBlob };
|
||||
} finally { closeSync(fd); }
|
||||
// The helper is the canonical registry capability boundary. It opens the exact
|
||||
// production snapshot.json and descriptor component-by-component, and MUST prove
|
||||
// the Git commit/blob identity; a pathname-shaped file is never sufficient.
|
||||
const repositoryRoot = join(dirname(root), "repo");
|
||||
const verified = this.helper("verified-snapshot", {
|
||||
snapshots_root: root, repository_root: repositoryRoot,
|
||||
workspace_revision: match[1], workspace_id: match[2],
|
||||
});
|
||||
if (!verified || typeof verified.source !== "string"
|
||||
|| verified.sha256 !== digest(verified.source) || verified.snapshot_path !== path) {
|
||||
throw new Error("workspace snapshot integrity check failed");
|
||||
}
|
||||
const workspace = validateOperationalWorkspace(parseWorkspaceYaml(verified.source));
|
||||
if (workspace.workspace.id !== match[2] || typeof verified.descriptor_git_blob !== "string") {
|
||||
throw new Error("workspace snapshot integrity check failed");
|
||||
}
|
||||
return {
|
||||
workspace, workspaceId: match[2], workspaceRevision: match[1],
|
||||
revisionContentRoot: join(root, match[1]), digest: verified.sha256,
|
||||
descriptorBlob: verified.descriptor_git_blob,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user