fix: harden runtime config lease identity publication
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
@@ -32,7 +32,7 @@ llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`;
|
||||
|
||||
function fixture() {
|
||||
function fixture(extraEnv: Record<string, string> = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), "runtime-config-lease-"));
|
||||
const canonicalDescriptor = serializeWorkspaceYaml(parseWorkspaceYaml(descriptor));
|
||||
const snapshots = join(root, "snapshots");
|
||||
@@ -73,13 +73,13 @@ function fixture() {
|
||||
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,
|
||||
THT_WS_ABC_DWH_PASSWORD_FILE: secret, ...extraEnv,
|
||||
}, secretRoots: [root], semanticRuntime: {
|
||||
internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024,
|
||||
},
|
||||
});
|
||||
return { root, snapshotPath, factory, canonicalDescriptor };
|
||||
return { root, repo, snapshotPath, factory, canonicalDescriptor, snapshotManifest: join(snapshotsDir, "snapshot.json") };
|
||||
}
|
||||
|
||||
test("session and maintenance share deterministic bytes and path", () => {
|
||||
@@ -141,3 +141,113 @@ test("same-byte replacement of the registry descriptor is refused", () => {
|
||||
first.release();
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("manifest binds the complete canonical destination directory chain", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const manifest = JSON.parse(readFileSync(lease.manifestPath, "utf8"));
|
||||
expect(manifest.directory_identities.length).toBeGreaterThan(5);
|
||||
expect(manifest.directory_identities.map((entry: { path: string }) => entry.path)).toContain(
|
||||
`${process.platform === "darwin" ? "/private" : ""}${join(f.root, "data", "sessions", workspace, "preprocessing")}`,
|
||||
);
|
||||
expect(manifest.directory_identities.every((entry: Record<string, string>) =>
|
||||
["path", "dev", "ino", "mode", "uid"].every((key) => typeof entry[key] === "string"),
|
||||
)).toBe(true);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("raw Git identity ignores replacement refs", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const evil = f.canonicalDescriptor.replace("database: analytics", "database: evil");
|
||||
chmodSync(join(f.repo, "workspaces", `${workspace}.yaml`), 0o600);
|
||||
writeFileSync(join(f.repo, "workspaces", `${workspace}.yaml`), evil);
|
||||
execFileSync("git", ["add", "."], { cwd: f.repo });
|
||||
execFileSync("git", ["commit", "-m", "evil"], { cwd: f.repo });
|
||||
const evilCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: f.repo, encoding: "utf8" }).trim();
|
||||
const oldCommit = JSON.parse(readFileSync(f.snapshotManifest, "utf8")).head;
|
||||
execFileSync("git", ["replace", oldCommit, evilCommit], { cwd: f.repo });
|
||||
chmodSync(f.snapshotPath, 0o600);
|
||||
writeFileSync(f.snapshotPath, evil);
|
||||
chmodSync(f.snapshotPath, 0o400);
|
||||
const snapshot = JSON.parse(readFileSync(f.snapshotManifest, "utf8"));
|
||||
snapshot.files[`${workspace}.yaml`] = createHash("sha256").update(evil).digest("hex");
|
||||
chmodSync(f.snapshotManifest, 0o600);
|
||||
writeFileSync(f.snapshotManifest, JSON.stringify(snapshot));
|
||||
chmodSync(f.snapshotManifest, 0o400);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/Git descriptor|integrity|identity/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("replacement of canonical destination directories is refused", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const original = join(f.root, "data", "sessions", workspace);
|
||||
const moved = `${original}.moved`;
|
||||
renameSync(original, moved);
|
||||
mkdirSync(join(original, "preprocessing", "runtime-config"), { recursive: true, mode: 0o700 });
|
||||
mkdirSync(join(original, "preprocessing", "runtime-config-manifests"), { recursive: true, mode: 0o700 });
|
||||
renameSync(join(moved, "preprocessing", "runtime-config", `${lease.workspaceRevision}.yaml`), join(original, "preprocessing", "runtime-config", `${lease.workspaceRevision}.yaml`));
|
||||
renameSync(join(moved, "preprocessing", "runtime-config-manifests", `${lease.workspaceRevision}.json`), join(original, "preprocessing", "runtime-config-manifests", `${lease.workspaceRevision}.json`));
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/changed|mismatch|same-revision|identity|trusted/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("rename faults fail closed and remove staging files", () => {
|
||||
const f = fixture({ THT_RUNTIME_CONFIG_RENAME_FAIL: "1" });
|
||||
try {
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/rename|failed/i);
|
||||
const runtime = join(f.root, "data", "sessions", workspace, "preprocessing");
|
||||
for (const dir of ["runtime-config", "runtime-config-manifests"]) {
|
||||
if (existsSync(join(runtime, dir))) expect(readdirSync(join(runtime, dir)).filter((name) => name.includes("staging")).length).toBe(0);
|
||||
}
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("session and operator outputs retain normalized private-host policy and binding", () => {
|
||||
const f = fixture({ THT_HTTP_PRIVATE_HOST_ALLOWLIST: "internal.example,warehouse.example" });
|
||||
try {
|
||||
const session = f.factory.acquireSession(f.snapshotPath);
|
||||
const maintenance = f.factory.acquireMaintenance({ snapshotPath: f.snapshotPath });
|
||||
const output = readFileSync(session.path, "utf8");
|
||||
expect(output).toContain("http_private_host_allowlist");
|
||||
expect(output).toContain("- internal.example");
|
||||
expect(output).toContain("- warehouse.example");
|
||||
expect(output).toBe(readFileSync(maintenance.path, "utf8"));
|
||||
const manifest = JSON.parse(readFileSync(session.manifestPath, "utf8"));
|
||||
expect(manifest.config_dwh_binding).toEqual({
|
||||
workspace_id: expect.any(String), config_fingerprint: expect.any(String), input_fingerprint: expect.any(String),
|
||||
});
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("unexpected manifest fields are refused before handoff", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const manifest = JSON.parse(readFileSync(lease.manifestPath, "utf8"));
|
||||
manifest.unexpected = true;
|
||||
chmodSync(lease.manifestPath, 0o600);
|
||||
writeFileSync(lease.manifestPath, JSON.stringify(manifest));
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/manifest|invalid|changed/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("runtime config symlink replacement is refused", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const lease = f.factory.acquireSession(f.snapshotPath);
|
||||
const replacement = `${lease.path}.real`;
|
||||
writeFileSync(replacement, readFileSync(lease.path), { mode: 0o400 });
|
||||
chmodSync(lease.path, 0o600);
|
||||
rmSync(lease.path);
|
||||
// A no-follow handoff must never consume this pathname.
|
||||
execFileSync("ln", ["-s", replacement, lease.path]);
|
||||
expect(() => f.factory.acquireSession(f.snapshotPath)).toThrow(/trusted|changed|configuration|symbolic/i);
|
||||
} finally { rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user