fix(auth): validate stopped workspace restore

This commit is contained in:
2026-08-18 00:06:28 +02:00
parent e8b9995ed0
commit 0651f3316f
6 changed files with 300 additions and 11 deletions
+13 -2
View File
@@ -18,7 +18,8 @@ import { WorkspaceRegistry } from "./workspaces/registry.js";
import { WorkspaceSecretStore } from "./workspaces/secret-store.js";
type OperatorAction = "maintenance-activate" | "maintenance-deactivate" | "maintenance-status"
| "session-inventory" | "workflow-doctor" | "pi-options" | "pi-test" | "effective-settings";
| "session-inventory" | "workflow-doctor" | "workspace-integrity"
| "pi-options" | "pi-test" | "effective-settings";
const lifecyclePrincipal: PrincipalContext = {
issuer: "tht-operator-command",
@@ -86,6 +87,15 @@ async function workflowDiagnostics(config: AppConfig): Promise<{ ready: true; wo
return { ready: true, workspaces: revisions.length };
}
async function workspaceIntegrity(config: AppConfig): Promise<{
ready: true;
state: "uninitialized" | "active";
workspaces: number;
}> {
const integrity = await new WorkspaceRegistry(config.workspaceRegistry).verifyStoredState();
return { ready: true, ...integrity };
}
export async function runOperatorAction(
action: OperatorAction,
config: AppConfig,
@@ -98,6 +108,7 @@ export async function runOperatorAction(
}
if (action === "session-inventory") return await sessionInventory(config);
if (action === "workflow-doctor") return await workflowDiagnostics(config);
if (action === "workspace-integrity") return await workspaceIntegrity(config);
if (action === "effective-settings") return effectiveSettings(config, loadSettings(config));
const service = createPiManagement(config, { listModels: createPiModelLister(config) });
if (action === "pi-options") return await service.options();
@@ -109,7 +120,7 @@ async function main(): Promise<void> {
const action = process.argv[2] as OperatorAction | undefined;
if (!action || ![
"maintenance-activate", "maintenance-deactivate", "maintenance-status", "session-inventory",
"workflow-doctor", "pi-options", "pi-test", "effective-settings",
"workflow-doctor", "workspace-integrity", "pi-options", "pi-test", "effective-settings",
].includes(action)) throw new Error("invalid operator action");
const result = await runOperatorAction(action, loadConfig(process.env));
process.stdout.write(`${JSON.stringify(result)}\n`);
+61
View File
@@ -31,6 +31,11 @@ export interface WorkspaceRevision {
snapshotPath: string;
}
export interface StoredWorkspaceIntegrity {
state: "uninitialized" | "active";
workspaces: number;
}
export interface SessionRevisionLease {
workspace: WorkspaceDescriptor;
revision: WorkspaceRevision;
@@ -180,6 +185,48 @@ export class WorkspaceRegistry {
});
}
/**
* Validate only persisted local registry state. Restore uses this path while the installation
* is stopped: it must neither contact Git nor turn a never-used registry into initialized state.
*/
async verifyStoredState(): Promise<StoredWorkspaceIntegrity> {
await this.repository.ensureLayout();
return await this.lock.run(async () => {
try {
const stateEntries = await readdir(this.repository.statePath, { withFileTypes: true });
if (stateEntries.some((entry) => (
entry.name !== "active.json" || !entry.isFile() || entry.isSymbolicLink()
))) throw new Error("workspace state directory is partial");
const snapshotEntries = await readdir(this.repository.snapshotsPath, { withFileTypes: true });
for (const entry of snapshotEntries) {
const isRuntime = entry.name === "runtime";
const isSnapshot = /^[0-9a-f]{40}$/.test(entry.name);
if ((!isRuntime && !isSnapshot) || !entry.isDirectory() || entry.isSymbolicLink()) {
throw new Error("workspace snapshot directory is partial");
}
}
const hasActiveState = stateEntries.length === 1;
if (!hasActiveState) {
if (snapshotEntries.some((entry) => entry.name !== "runtime") || this.storedPathExists(this.repository.repoPath)) {
throw new Error("workspace registry is partially initialized");
}
return { state: "uninitialized", workspaces: 0 };
}
const active = await this.activeState();
for (const entry of snapshotEntries) {
if (entry.name === "runtime" || entry.name === active.head) continue;
await this.snapshotState(entry.name);
}
return { state: "active", workspaces: active.revisions.length };
} catch (error) {
throw workspaceError(error);
}
});
}
async read(id: string): Promise<{ workspace: WorkspaceDescriptor; revision: WorkspaceRevision }> {
const state = await this.activeState();
const revision = state.revisions.find((candidate) => candidate.id === id);
@@ -737,6 +784,20 @@ export class WorkspaceRegistry {
}
}
private storedPathExists(path: string): boolean {
try {
const entry = lstatSync(path);
if (!entry.isDirectory() || entry.isSymbolicLink()) {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace registry path is invalid");
}
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
if (error instanceof WorkspaceRegistryError) throw error;
throw new WorkspaceRegistryError("workspace_invalid", "Workspace registry path is unavailable");
}
}
private pathIsMissing(path: string): boolean {
try {
lstatSync(path);