fix: preserve deterministic runtime secret leases
This commit is contained in:
@@ -7,6 +7,7 @@ import { loadConfig } from "./config.js";
|
||||
import { ThtRunner } from "./tht/tht-runner.js";
|
||||
import { WorkspaceRegistry } from "./workspaces/registry.js";
|
||||
import { publishDeterministicRuntimeConfigLease, renderActiveWorkspaceRuntime } from "./workspaces/runtime-config-lease.js";
|
||||
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
|
||||
import { WorkspacePreprocessingService, type WorkspaceOperationResult } from "./workspaces/preprocessing-service.js";
|
||||
import type { SessionInventoryRow } from "./workspaces/preprocessing-state.js";
|
||||
|
||||
@@ -213,6 +214,11 @@ async function readSessionInventory(dataRoot: string, workspaceId: string): Prom
|
||||
function createProductionService(): WorkspacePreprocessingService {
|
||||
const config = loadConfig(process.env);
|
||||
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
||||
const workspaceSecretStore = new WorkspaceSecretStore({
|
||||
root: config.workspaceSecretStoreRoot,
|
||||
runtimeRoot: config.workspaceSecretRuntimeRoot,
|
||||
installationId: config.workspaceRegistry.installationId,
|
||||
});
|
||||
const runner = new ThtRunner({
|
||||
thtBin: config.thtBin,
|
||||
harnessDir: config.harnessDir,
|
||||
@@ -222,6 +228,7 @@ function createProductionService(): WorkspacePreprocessingService {
|
||||
secretRoots: config.workspaceRegistry.secretRoots,
|
||||
secretsFile: config.secretsFile,
|
||||
secretFiles: config.secretFiles,
|
||||
workspaceSecretStore,
|
||||
semanticRuntime: {
|
||||
internalQdrantUrl: config.internalQdrantUrl,
|
||||
internalEmbeddingUrl: config.internalEmbeddingUrl,
|
||||
@@ -263,6 +270,7 @@ function createProductionService(): WorkspacePreprocessingService {
|
||||
internalEmbeddingModel: config.internalEmbeddingModel,
|
||||
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
|
||||
},
|
||||
workspaceSecretStore,
|
||||
});
|
||||
return {
|
||||
workspace: active.workspace,
|
||||
|
||||
@@ -458,6 +458,7 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
dataRoot: string;
|
||||
secretRoots: readonly string[];
|
||||
semanticRuntime: SemanticRuntimeConfig;
|
||||
workspaceSecretStore?: WorkspaceSecretStore;
|
||||
}): Promise<DeterministicRuntimeConfigLease> {
|
||||
const rendered = await renderActiveWorkspaceRuntime(options);
|
||||
const publishedConfig = applyCollectionLifecycle(rendered.renderedConfig, "require_existing");
|
||||
@@ -542,7 +543,7 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
effectiveConfigIdentity: effectiveConfigIdentityValue,
|
||||
configFingerprint: configFingerprintValue,
|
||||
inputFingerprint: inputFingerprintValue,
|
||||
release: () => undefined,
|
||||
release: () => rendered.releaseSecrets(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -605,6 +606,6 @@ export async function publishDeterministicRuntimeConfigLease(options: {
|
||||
effectiveConfigIdentity: effectiveConfigIdentityValue,
|
||||
configFingerprint: configFingerprintValue,
|
||||
inputFingerprint: inputFingerprintValue,
|
||||
release: () => undefined,
|
||||
release: () => rendered.releaseSecrets(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import {
|
||||
fchmodSync,
|
||||
fsyncSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
const STORE_ERROR = "Workspace secret store is unavailable.";
|
||||
@@ -94,6 +93,7 @@ export class WorkspaceSecretStore {
|
||||
private readonly maxSecretBytes: number;
|
||||
private readonly keyPath: string;
|
||||
private readonly vaultPath: string;
|
||||
private readonly materializationReferences = new Map<string, number>();
|
||||
|
||||
constructor(options: WorkspaceSecretStoreOptions) {
|
||||
if (!options.installationId.trim()) throw new Error("Installation identifier is required.");
|
||||
@@ -184,11 +184,37 @@ export class WorkspaceSecretStore {
|
||||
}
|
||||
|
||||
let directory: string | undefined;
|
||||
let retained = false;
|
||||
try {
|
||||
const vault = this.readVault();
|
||||
const key = this.readKey();
|
||||
directory = mkdtempSync(join(this.runtimeRoot, "lease-"));
|
||||
chmodSync(directory, 0o700);
|
||||
const workspaceEntries = requirementIds
|
||||
.map((requirementId) => vault.entries[entryKey(workspaceId, requirementId)])
|
||||
.filter((entry): entry is EncryptedEntry => entry !== undefined)
|
||||
.sort((left, right) => left.requirementId.localeCompare(right.requirementId));
|
||||
const materializationId = createHash("sha256")
|
||||
.update(this.installationId)
|
||||
.update("\0")
|
||||
.update(workspaceId)
|
||||
.update("\0")
|
||||
.update(JSON.stringify(workspaceEntries))
|
||||
.digest("hex")
|
||||
.slice(0, 24);
|
||||
directory = join(
|
||||
this.runtimeRoot,
|
||||
`workspace-${createHash("sha256").update(workspaceId).digest("hex").slice(0, 16)}-${materializationId}`,
|
||||
);
|
||||
const references = this.materializationReferences.get(directory) ?? 0;
|
||||
if (references === 0) {
|
||||
try {
|
||||
mkdirSync(directory, { recursive: false, mode: 0o700 });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
mkdirSync(directory, { recursive: false, mode: 0o700 });
|
||||
}
|
||||
chmodSync(directory, 0o700);
|
||||
}
|
||||
const files = new Map<string, string>();
|
||||
for (const requirementId of [...new Set(requirementIds)]) {
|
||||
const entry = vault.entries[entryKey(workspaceId, requirementId)];
|
||||
@@ -207,18 +233,24 @@ export class WorkspaceSecretStore {
|
||||
decipher.update(Buffer.from(entry.ciphertext, "base64")),
|
||||
decipher.final(),
|
||||
]);
|
||||
const path = join(directory, randomBytes(16).toString("hex"));
|
||||
const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o400);
|
||||
try {
|
||||
fchmodSync(fd, 0o400);
|
||||
writeFileSync(fd, plaintext);
|
||||
fsyncSync(fd);
|
||||
} finally {
|
||||
const path = join(directory, createHash("sha256").update(requirementId).digest("hex"));
|
||||
if (references === 0) {
|
||||
const fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o400);
|
||||
try {
|
||||
fchmodSync(fd, 0o400);
|
||||
writeFileSync(fd, plaintext);
|
||||
fsyncSync(fd);
|
||||
} finally {
|
||||
plaintext.fill(0);
|
||||
closeSync(fd);
|
||||
}
|
||||
} else {
|
||||
plaintext.fill(0);
|
||||
closeSync(fd);
|
||||
}
|
||||
files.set(requirementId, path);
|
||||
}
|
||||
this.materializationReferences.set(directory, references + 1);
|
||||
retained = true;
|
||||
let released = false;
|
||||
const leasedDirectory = directory;
|
||||
return {
|
||||
@@ -226,11 +258,19 @@ export class WorkspaceSecretStore {
|
||||
release: () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
rmSync(leasedDirectory, { recursive: true, force: true });
|
||||
const remaining = (this.materializationReferences.get(leasedDirectory) ?? 1) - 1;
|
||||
if (remaining > 0) {
|
||||
this.materializationReferences.set(leasedDirectory, remaining);
|
||||
} else {
|
||||
this.materializationReferences.delete(leasedDirectory);
|
||||
rmSync(leasedDirectory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
if (directory !== undefined) rmSync(directory, { recursive: true, force: true });
|
||||
if (directory !== undefined && !retained && !this.materializationReferences.has(directory)) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
throw new Error(STORE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user