fix(doctor): complete v6.11 automated import guard

Completes the v6.11 adaptation of cd9db5ed9a after prerequisite 2b3dc3042f by disabling cross-state approval imports for implicit and automated doctor callers.
This commit is contained in:
Dallin Romney
2026-07-14 02:13:05 -07:00
parent a7dba8d7b1
commit 37f5fa9de2
3 changed files with 28 additions and 10 deletions
+20 -1
View File
@@ -103,6 +103,23 @@ function hasBundledChannelLegacyStateMigrationInputs(stateDir: string, oauthDir:
return dirHasFile(oauthDir, isLegacyWhatsAppAuthFile);
}
function hasCrossStateDirApprovalMigrationInputs(stateDir: string): boolean {
if (!process.env.OPENCLAW_STATE_DIR?.trim()) {
return false;
}
const homeDir = resolveRequiredHomeDir(process.env, os.homedir);
const defaultStateDir = resolveNewStateDir(() => homeDir);
if (path.resolve(defaultStateDir) === path.resolve(stateDir)) {
return false;
}
const execApprovalsSource = path.join(defaultStateDir, "exec-approvals.json");
const execApprovalsTarget = path.join(stateDir, "exec-approvals.json");
return (
(fileOrDirExists(execApprovalsSource) && !fileOrDirExists(execApprovalsTarget)) ||
fileOrDirExists(path.join(defaultStateDir, "plugin-binding-approvals.json"))
);
}
function hasPendingSqliteSidecarArchive(sourcePath: string): boolean {
return (
fileOrDirExists(`${sourcePath}.migrated`) &&
@@ -137,7 +154,8 @@ function hasLegacyStateMigrationInputs(): boolean {
sqliteSidecarPaths.some(
(sourcePath) => fileOrDirExists(sourcePath) || hasPendingSqliteSidecarArchive(sourcePath),
) ||
hasBundledChannelLegacyStateMigrationInputs(stateDir, oauthDir)
hasBundledChannelLegacyStateMigrationInputs(stateDir, oauthDir) ||
hasCrossStateDirApprovalMigrationInputs(stateDir)
);
}
@@ -195,6 +213,7 @@ export async function ensureConfigReady(params: {
migrateState: true,
migrateLegacyConfig: false,
invalidConfigNote: false,
crossStateDirImports: false,
...(params.beforeStateMigrations
? { beforeStateMigrations: params.beforeStateMigrations }
: {}),
+2 -2
View File
@@ -11,7 +11,6 @@ import {
noteOpencodeProviderOverrides,
} from "./doctor-config-analysis.js";
import { runDoctorConfigPreflight } from "./doctor-config-preflight.js";
import { normalizeCompatibilityConfigValues } from "./doctor/shared/legacy-config-core-migrate.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
import { emitDoctorNotes, sanitizeDoctorNote } from "./doctor/emit-notes.js";
import { finalizeDoctorConfigFlow } from "./doctor/finalize-config-flow.js";
@@ -24,6 +23,7 @@ import {
collectMissingDefaultAccountBindingWarnings,
collectMissingExplicitDefaultAccountWarnings,
} from "./doctor/shared/default-account-warnings.js";
import { normalizeCompatibilityConfigValues } from "./doctor/shared/legacy-config-core-migrate.js";
function hasLegacyInternalHookHandlers(raw: unknown): boolean {
const handlers = (raw as { hooks?: { internal?: { handlers?: unknown } } })?.hooks?.internal
@@ -145,7 +145,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
const preflight = await runDoctorConfigPreflight({
repairPrefixedConfig: shouldRepair,
recoverCorruptTargetStore: shouldRepair,
crossStateDirImports: shouldRepair,
crossStateDirImports: shouldRepair && params.options.crossStateDirImports === true,
});
const snapshot = preflight.snapshot;
const baseCfg = preflight.baseConfig;
+6 -7
View File
@@ -514,16 +514,15 @@ async function runLegacyStateHealth(ctx: DoctorHealthFlowContext): Promise<void>
const { detectLegacyStateMigrations, runLegacyStateMigrations } =
await import("../commands/doctor-state-migrations.js");
const { note } = await loadNoteModule();
// Cross-state-dir imports (default home dir -> OPENCLAW_STATE_DIR) are
// allowed here only when the operator either confirms the previewed plan
// interactively or asked for repair; a bare non-interactive doctor stays
// read-only toward the default state dir.
// Only a direct operator-owned doctor may inspect the default state dir for
// imports. Automated repair callers explicitly lack this capability so a
// temporary OPENCLAW_STATE_DIR cannot capture and archive production trust.
const operatorCanApproveCrossStateDirImports =
ctx.prompter.repairMode.canPrompt || ctx.prompter.shouldRepair;
const legacyState = await detectLegacyStateMigrations({
cfg: ctx.cfg,
crossStateDirImports:
ctx.options.nonInteractive !== true ||
ctx.options.repair === true ||
ctx.options.yes === true,
ctx.options.crossStateDirImports === true && operatorCanApproveCrossStateDirImports,
});
if (legacyState.warnings.length > 0) {
note(legacyState.warnings.join("\n"), "Doctor warnings");