fix: harden runtime config snapshot publication
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
type RuntimePaths,
|
||||
type SemanticRuntimeConfig,
|
||||
} from "./runtime-renderer.js";
|
||||
import { parseWorkspaceYaml, validateOperationalWorkspace, type WorkspaceDescriptor } from "./schema.js";
|
||||
import { parseWorkspaceYaml, serializeWorkspaceYaml, validateOperationalWorkspace, type WorkspaceDescriptor } from "./schema.js";
|
||||
|
||||
export interface RuntimeConfigLease {
|
||||
path: string;
|
||||
@@ -50,6 +50,8 @@ interface SnapshotIdentity {
|
||||
revisionContentRoot: string;
|
||||
digest: string;
|
||||
descriptorBlob?: string;
|
||||
descriptorDev: string;
|
||||
descriptorIno: string;
|
||||
}
|
||||
interface PublishedIdentity {
|
||||
path: string;
|
||||
@@ -154,6 +156,8 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
workspace_id: snapshot.workspaceId, workspace_revision: snapshot.workspaceRevision,
|
||||
descriptor_git_blob: snapshot.descriptorBlob!,
|
||||
descriptor_sha256: snapshot.digest,
|
||||
descriptor_dev: snapshot.descriptorDev,
|
||||
descriptor_ino: snapshot.descriptorIno,
|
||||
config_sha256: renderedDigest,
|
||||
config_dwh_binding: this.computeBinding(rendered),
|
||||
};
|
||||
@@ -168,10 +172,14 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
|
||||
private helper(action: string, extra: Record<string, unknown>): any {
|
||||
const python = join(this.input.harnessDir, ".venv", "bin", "python");
|
||||
const executable = existsSync(python) ? python : (process.env.PYTHON ?? "python3");
|
||||
const modulePath = existsSync(join(this.input.harnessDir, "tht", "runtime_config_lease_io.py"))
|
||||
? join(this.input.harnessDir, "tht", "runtime_config_lease_io.py")
|
||||
: join(process.cwd(), "../harness/tht/runtime_config_lease_io.py");
|
||||
// Fixtures may provide a temporary harness directory; still execute the real
|
||||
// project helper environment, never a fabricated TypeScript binding.
|
||||
const projectPython = join(dirname(dirname(modulePath)), ".venv", "bin", "python");
|
||||
const executable = existsSync(python) ? python
|
||||
: existsSync(projectPython) ? projectPython : (process.env.PYTHON ?? "python3");
|
||||
const helperArgs = existsSync(modulePath) ? [modulePath] : ["-m", "tht.runtime_config_lease_io"];
|
||||
const result = spawnSync(executable, helperArgs, { cwd: this.input.harnessDir,
|
||||
input: JSON.stringify({ action, ...extra }), encoding: "utf8",
|
||||
@@ -188,12 +196,10 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
try {
|
||||
const value = this.helper("binding", { config_hex: Buffer.from(content).toString("hex") });
|
||||
if (value && typeof value.workspace_id === "string" && typeof value.config_fingerprint === "string" && typeof value.input_fingerprint === "string") return value;
|
||||
throw new Error("runtime config binding helper returned malformed output");
|
||||
} catch (error) {
|
||||
// Development fixtures may intentionally omit the harness virtualenv. Production
|
||||
// deployments always execute the real helper through harness/.venv/bin/python.
|
||||
if (existsSync(join(this.input.harnessDir, ".venv", "bin", "python"))) throw error;
|
||||
throw error instanceof Error ? error : new Error("runtime config binding failed");
|
||||
}
|
||||
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; manifest_sha256:string} {
|
||||
@@ -231,11 +237,25 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
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) {
|
||||
if (!verified || typeof verified.source !== "string" || typeof verified.git_source !== "string"
|
||||
|| verified.sha256 !== digest(verified.source) || verified.snapshot_path !== path
|
||||
|| !/^\d+$/.test(String(verified.descriptor_dev)) || !/^\d+$/.test(String(verified.descriptor_ino))) {
|
||||
throw new Error("workspace snapshot integrity check failed");
|
||||
}
|
||||
const workspace = validateOperationalWorkspace(parseWorkspaceYaml(verified.source));
|
||||
// The registry's production canonicalizer is the sole descriptor equivalence
|
||||
// rule. Raw token containment is not identity: it permits changed values.
|
||||
let workspace: WorkspaceDescriptor;
|
||||
let gitWorkspace: WorkspaceDescriptor;
|
||||
try {
|
||||
workspace = validateOperationalWorkspace(parseWorkspaceYaml(verified.source));
|
||||
gitWorkspace = validateOperationalWorkspace(parseWorkspaceYaml(verified.git_source));
|
||||
if (serializeWorkspaceYaml(workspace) !== serializeWorkspaceYaml(gitWorkspace)
|
||||
|| serializeWorkspaceYaml(workspace) !== verified.source) {
|
||||
throw new Error("canonical descriptor differs from Git");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`workspace snapshot integrity check failed: ${error instanceof Error ? error.message : "invalid descriptor"}`);
|
||||
}
|
||||
if (workspace.workspace.id !== match[2] || typeof verified.descriptor_git_blob !== "string") {
|
||||
throw new Error("workspace snapshot integrity check failed");
|
||||
}
|
||||
@@ -243,6 +263,7 @@ export class WorkspaceRuntimeConfigLeaseFactory {
|
||||
workspace, workspaceId: match[2], workspaceRevision: match[1],
|
||||
revisionContentRoot: join(root, match[1]), digest: verified.sha256,
|
||||
descriptorBlob: verified.descriptor_git_blob,
|
||||
descriptorDev: String(verified.descriptor_dev), descriptorIno: String(verified.descriptor_ino),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user