mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 08:37:41 +00:00
fix(agents): harden compaction and reset safety
Co-authored-by: jaden-clovervnd <91520439+jaden-clovervnd@users.noreply.github.com> Co-authored-by: Sid <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: Marcus Widing <245375637+widingmarcus-cyber@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import fsPromises from "node:fs/promises";
|
||||
import nodePath from "node:path";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readConfigFileSnapshot, resolveGatewayPort, writeConfigFile } from "../config/config.js";
|
||||
@@ -332,6 +334,32 @@ export async function runConfigureWizard(
|
||||
runtime,
|
||||
);
|
||||
workspaceDir = resolveUserPath(String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE);
|
||||
if (!snapshot.exists) {
|
||||
const indicators = ["MEMORY.md", "memory", ".git"].map((name) =>
|
||||
nodePath.join(workspaceDir, name),
|
||||
);
|
||||
const hasExistingContent = (
|
||||
await Promise.all(
|
||||
indicators.map(async (candidate) => {
|
||||
try {
|
||||
await fsPromises.access(candidate);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).some(Boolean);
|
||||
if (hasExistingContent) {
|
||||
note(
|
||||
[
|
||||
`Existing workspace detected at ${workspaceDir}`,
|
||||
"Existing files are preserved. Missing templates may be created, never overwritten.",
|
||||
].join("\n"),
|
||||
"Existing workspace",
|
||||
);
|
||||
}
|
||||
}
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
|
||||
@@ -98,6 +98,7 @@ export type OnboardOptions = {
|
||||
/** Required for non-interactive onboarding; skips the interactive risk prompt when true. */
|
||||
acceptRisk?: boolean;
|
||||
reset?: boolean;
|
||||
resetScope?: ResetScope;
|
||||
authChoice?: AuthChoice;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
tokenProvider?: string;
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
runInteractiveOnboarding: vi.fn(async () => {}),
|
||||
runNonInteractiveOnboarding: vi.fn(async () => {}),
|
||||
readConfigFileSnapshot: vi.fn(async () => ({ exists: false, valid: false, config: {} })),
|
||||
handleReset: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-interactive.js", () => ({
|
||||
@@ -14,6 +16,15 @@ vi.mock("./onboard-non-interactive.js", () => ({
|
||||
runNonInteractiveOnboarding: mocks.runNonInteractiveOnboarding,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshot: mocks.readConfigFileSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-helpers.js", () => ({
|
||||
DEFAULT_WORKSPACE: "~/.openclaw/workspace",
|
||||
handleReset: mocks.handleReset,
|
||||
}));
|
||||
|
||||
const { onboardCommand } = await import("./onboard.js");
|
||||
|
||||
function makeRuntime(): RuntimeEnv {
|
||||
@@ -27,6 +38,7 @@ function makeRuntime(): RuntimeEnv {
|
||||
describe("onboardCommand", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({ exists: false, valid: false, config: {} });
|
||||
});
|
||||
|
||||
it("fails fast for invalid secret-input-mode before onboarding starts", async () => {
|
||||
@@ -46,4 +58,55 @@ describe("onboardCommand", () => {
|
||||
expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled();
|
||||
expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defaults --reset to config+creds+sessions scope", async () => {
|
||||
const runtime = makeRuntime();
|
||||
|
||||
await onboardCommand(
|
||||
{
|
||||
reset: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(mocks.handleReset).toHaveBeenCalledWith(
|
||||
"config+creds+sessions",
|
||||
expect.any(String),
|
||||
runtime,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts explicit --reset-scope full", async () => {
|
||||
const runtime = makeRuntime();
|
||||
|
||||
await onboardCommand(
|
||||
{
|
||||
reset: true,
|
||||
resetScope: "full",
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(mocks.handleReset).toHaveBeenCalledWith("full", expect.any(String), runtime);
|
||||
});
|
||||
|
||||
it("fails fast for invalid --reset-scope", async () => {
|
||||
const runtime = makeRuntime();
|
||||
|
||||
await onboardCommand(
|
||||
{
|
||||
reset: true,
|
||||
resetScope: "invalid" as never,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
'Invalid --reset-scope. Use "config", "config+creds+sessions", or "full".',
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(mocks.handleReset).not.toHaveBeenCalled();
|
||||
expect(mocks.runInteractiveOnboarding).not.toHaveBeenCalled();
|
||||
expect(mocks.runNonInteractiveOnboarding).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@ import { isDeprecatedAuthChoice, normalizeLegacyOnboardAuthChoice } from "./auth
|
||||
import { DEFAULT_WORKSPACE, handleReset } from "./onboard-helpers.js";
|
||||
import { runInteractiveOnboarding } from "./onboard-interactive.js";
|
||||
import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js";
|
||||
import type { OnboardOptions } from "./onboard-types.js";
|
||||
import type { OnboardOptions, ResetScope } from "./onboard-types.js";
|
||||
|
||||
const VALID_RESET_SCOPES = new Set<ResetScope>(["config", "config+creds+sessions", "full"]);
|
||||
|
||||
export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) {
|
||||
assertSupportedRuntime(runtime);
|
||||
@@ -45,6 +47,12 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedOpts.resetScope && !VALID_RESET_SCOPES.has(normalizedOpts.resetScope)) {
|
||||
runtime.error('Invalid --reset-scope. Use "config", "config+creds+sessions", or "full".');
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) {
|
||||
runtime.error(
|
||||
[
|
||||
@@ -62,7 +70,8 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv =
|
||||
const baseConfig = snapshot.valid ? snapshot.config : {};
|
||||
const workspaceDefault =
|
||||
normalizedOpts.workspace ?? baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE;
|
||||
await handleReset("full", resolveUserPath(workspaceDefault), runtime);
|
||||
const resetScope: ResetScope = normalizedOpts.resetScope ?? "config+creds+sessions";
|
||||
await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime);
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
|
||||
Reference in New Issue
Block a user