fix(cli): enforce full tool profile in dench bootstrap
This commit is contained in:
parent
2c5e5a8ac1
commit
1c93a3b525
@ -47,7 +47,7 @@ function createWebProfilesResponse(params?: {
|
||||
payload?: { profiles?: unknown[]; activeProfile?: string };
|
||||
}): Response {
|
||||
const status = params?.status ?? 200;
|
||||
const payload = params?.payload ?? { profiles: [], activeProfile: "ironclaw" };
|
||||
const payload = params?.payload ?? { profiles: [], activeProfile: "dench" };
|
||||
return {
|
||||
status,
|
||||
json: async () => payload,
|
||||
@ -56,12 +56,13 @@ function createWebProfilesResponse(params?: {
|
||||
|
||||
function createTempStateDir(): string {
|
||||
const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const dir = path.join(os.tmpdir(), `ironclaw-bootstrap-${suffix}`);
|
||||
const dir = path.join(os.tmpdir(), `denchclaw-bootstrap-${suffix}`);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeBootstrapFixtures(stateDir: string): void {
|
||||
mkdirSync(stateDir, { recursive: true });
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@ -137,6 +138,7 @@ async function withForcedStdinTty<T>(isTTY: boolean, fn: () => Promise<T>): Prom
|
||||
describe("bootstrapCommand always-onboard behavior", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
const spawnMock = vi.mocked(spawn);
|
||||
let homeDir = "";
|
||||
let stateDir = "";
|
||||
let spawnCalls: SpawnCall[] = [];
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
@ -148,7 +150,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
let alwaysHealthFail = false;
|
||||
|
||||
beforeEach(() => {
|
||||
stateDir = createTempStateDir();
|
||||
homeDir = createTempStateDir();
|
||||
stateDir = path.join(homeDir, ".openclaw-dench");
|
||||
writeBootstrapFixtures(stateDir);
|
||||
spawnCalls = [];
|
||||
forceGlobalMissing = false;
|
||||
@ -158,7 +161,10 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
alwaysHealthFail = false;
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
OPENCLAW_PROFILE: "ironclaw",
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
OPENCLAW_HOME: homeDir,
|
||||
OPENCLAW_PROFILE: "dench",
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
VITEST: "true",
|
||||
};
|
||||
@ -256,7 +262,7 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
rmSync(stateDir, { recursive: true, force: true });
|
||||
rmSync(homeDir || stateDir, { recursive: true, force: true });
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@ -284,7 +290,7 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
expect(onboardCalls[0]?.args).toEqual(
|
||||
expect.arrayContaining([
|
||||
"--profile",
|
||||
"ironclaw",
|
||||
"dench",
|
||||
"onboard",
|
||||
"--install-daemon",
|
||||
"--non-interactive",
|
||||
@ -296,13 +302,13 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
expect(summary.onboarded).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts bootstrap --profile and propagates it to onboard subprocesses", async () => {
|
||||
it("ignores bootstrap --profile override and keeps dench profile (prevents profile drift)", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
process.env.OPENCLAW_PROFILE = "ironclaw";
|
||||
process.env.OPENCLAW_PROFILE = "dench";
|
||||
|
||||
const summary = await bootstrapCommand(
|
||||
{
|
||||
@ -317,8 +323,9 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
const onboardCall = spawnCalls.find(
|
||||
(call) => call.command === "openclaw" && call.args.includes("onboard"),
|
||||
);
|
||||
expect(onboardCall?.args).toEqual(expect.arrayContaining(["--profile", "team-a"]));
|
||||
expect(summary.profile).toBe("team-a");
|
||||
expect(onboardCall?.args).toEqual(expect.arrayContaining(["--profile", "dench"]));
|
||||
expect(onboardCall?.args.includes("team-a")).toBe(false);
|
||||
expect(summary.profile).toBe("dench");
|
||||
});
|
||||
|
||||
it("adds --reset to onboarding args when --force-onboard is requested", async () => {
|
||||
@ -485,8 +492,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
expect(summary.workspaceSeed?.reason).toBe("already-exists");
|
||||
expect(readFileSync(workspaceDbPath, "utf-8")).toBe("existing-db-content");
|
||||
const identityContent = readFileSync(identityPath, "utf-8");
|
||||
expect(identityContent).toContain("You are **Ironclaw**");
|
||||
expect(identityContent).toContain("~skills/crm/SKILL.md");
|
||||
expect(identityContent).toContain("You are **DenchClaw**");
|
||||
expect(identityContent).toContain(path.join(workspaceDir, "skills", "crm", "SKILL.md"));
|
||||
expect(identityContent).not.toContain("# stale identity");
|
||||
});
|
||||
|
||||
@ -529,18 +536,18 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
const identityPath = path.join(managedWorkspace, "IDENTITY.md");
|
||||
expect(existsSync(identityPath)).toBe(true);
|
||||
const identityContent = readFileSync(identityPath, "utf-8");
|
||||
expect(identityContent).toContain("You are **Ironclaw**");
|
||||
expect(identityContent).toContain("~skills/crm/SKILL.md");
|
||||
expect(identityContent).toContain("You are **DenchClaw**");
|
||||
expect(identityContent).toContain(path.join(managedWorkspace, "skills", "crm", "SKILL.md"));
|
||||
});
|
||||
|
||||
it("installs CRM skill into managed profile skills directory (keeps it out of editable workspace)", async () => {
|
||||
it("installs CRM skill into managed workspace skills directory (prevents state-root drift)", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const targetSkill = path.join(stateDir, "skills", "crm", "SKILL.md");
|
||||
const workspaceSkill = path.join(stateDir, "workspace", "skills", "crm", "SKILL.md");
|
||||
const targetSkill = path.join(stateDir, "workspace", "skills", "crm", "SKILL.md");
|
||||
const legacySkill = path.join(stateDir, "skills", "crm", "SKILL.md");
|
||||
expect(existsSync(targetSkill)).toBe(false);
|
||||
|
||||
await bootstrapCommand(
|
||||
@ -553,7 +560,7 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
);
|
||||
|
||||
expect(existsSync(targetSkill)).toBe(true);
|
||||
expect(existsSync(workspaceSkill)).toBe(false);
|
||||
expect(existsSync(legacySkill)).toBe(false);
|
||||
expect(readFileSync(targetSkill, "utf-8")).toContain("name: database-crm-system");
|
||||
});
|
||||
|
||||
@ -563,7 +570,7 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const targetDir = path.join(stateDir, "skills", "crm");
|
||||
const targetDir = path.join(stateDir, "workspace", "skills", "crm");
|
||||
const targetSkill = path.join(targetDir, "SKILL.md");
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
writeFileSync(targetSkill, "name: crm\n# custom\n");
|
||||
@ -609,17 +616,83 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
expect(workspaceConfigSetCalls.length).toBeGreaterThan(0);
|
||||
const lastArgs = workspaceConfigSetCalls.at(-1)?.args ?? [];
|
||||
expect(lastArgs).toEqual(
|
||||
expect.arrayContaining([
|
||||
"--profile",
|
||||
"ironclaw",
|
||||
"config",
|
||||
"set",
|
||||
"agents.defaults.workspace",
|
||||
]),
|
||||
expect.arrayContaining(["--profile", "dench", "config", "set", "agents.defaults.workspace"]),
|
||||
);
|
||||
const configuredWorkspace = lastArgs.at(-1) ?? "";
|
||||
expect(configuredWorkspace).toContain(path.join(".openclaw-ironclaw", "workspace"));
|
||||
expect(configuredWorkspace).not.toContain("workspace-ironclaw");
|
||||
expect(configuredWorkspace).toContain(path.join(".openclaw-dench", "workspace"));
|
||||
expect(configuredWorkspace).not.toContain("workspace-dench");
|
||||
});
|
||||
|
||||
it("forces tools.profile to full during bootstrap (prevents messaging-only tool drift)", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const toolsProfileSetCalls = spawnCalls.filter(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("tools.profile"),
|
||||
);
|
||||
|
||||
expect(toolsProfileSetCalls.length).toBeGreaterThan(0);
|
||||
const lastArgs = toolsProfileSetCalls.at(-1)?.args ?? [];
|
||||
expect(lastArgs).toEqual(
|
||||
expect.arrayContaining(["--profile", "dench", "config", "set", "tools.profile", "full"]),
|
||||
);
|
||||
expect(lastArgs).not.toContain("messaging");
|
||||
});
|
||||
|
||||
it("reapplies tools.profile full on repeated bootstrap runs (setup/restart safety)", async () => {
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
await bootstrapCommand(
|
||||
{
|
||||
nonInteractive: true,
|
||||
noOpen: true,
|
||||
skipUpdate: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const toolsProfileSetCalls = spawnCalls.filter(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("tools.profile"),
|
||||
);
|
||||
|
||||
expect(toolsProfileSetCalls).toHaveLength(2);
|
||||
for (const call of toolsProfileSetCalls) {
|
||||
expect(call.args).toEqual(
|
||||
expect.arrayContaining(["--profile", "dench", "config", "set", "tools.profile", "full"]),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps CRM in managed skills even when workspace path is custom", async () => {
|
||||
@ -641,8 +714,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
gateway: { mode: "local" },
|
||||
}),
|
||||
);
|
||||
const managedSkill = path.join(stateDir, "skills", "crm", "SKILL.md");
|
||||
const workspaceSkill = path.join(customWorkspace, "skills", "crm", "SKILL.md");
|
||||
const managedWorkspaceSkill = path.join(stateDir, "workspace", "skills", "crm", "SKILL.md");
|
||||
const customWorkspaceSkill = path.join(customWorkspace, "skills", "crm", "SKILL.md");
|
||||
|
||||
await bootstrapCommand(
|
||||
{
|
||||
@ -653,10 +726,8 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(existsSync(managedSkill)).toBe(true);
|
||||
expect(existsSync(workspaceSkill)).toBe(false);
|
||||
const managedWorkspaceSkill = path.join(stateDir, "workspace", "skills", "crm", "SKILL.md");
|
||||
expect(existsSync(managedWorkspaceSkill)).toBe(true);
|
||||
expect(existsSync(customWorkspaceSkill)).toBe(false);
|
||||
});
|
||||
|
||||
it("uses inherited stdio for onboarding in interactive mode (shows wizard prompts)", async () => {
|
||||
@ -778,11 +849,21 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
(call) =>
|
||||
call.command === "openclaw" && call.args.includes("gateway") && call.args.includes("start"),
|
||||
);
|
||||
const toolsProfileSetCall = spawnCalls.find(
|
||||
(call) =>
|
||||
call.command === "openclaw" &&
|
||||
call.args.includes("config") &&
|
||||
call.args.includes("set") &&
|
||||
call.args.includes("tools.profile"),
|
||||
);
|
||||
|
||||
expect(doctorFixCalled).toBe(true);
|
||||
expect(gatewayStopCalled).toBe(true);
|
||||
expect(gatewayInstallCalled).toBe(true);
|
||||
expect(gatewayStartCalled).toBe(true);
|
||||
expect(toolsProfileSetCall?.args).toEqual(
|
||||
expect.arrayContaining(["--profile", "dench", "config", "set", "tools.profile", "full"]),
|
||||
);
|
||||
expect(summary.gatewayReachable).toBe(true);
|
||||
expect(summary.gatewayAutoFix?.attempted).toBe(true);
|
||||
expect(summary.gatewayAutoFix?.recovered).toBe(true);
|
||||
@ -798,7 +879,7 @@ describe("bootstrapCommand always-onboard behavior", () => {
|
||||
}
|
||||
return createWebProfilesResponse({
|
||||
status: 200,
|
||||
payload: { profiles: [], activeProfile: "ironclaw" },
|
||||
payload: { profiles: [], activeProfile: "dench" },
|
||||
});
|
||||
}
|
||||
if (url.includes("127.0.0.1:3101/api/profiles")) {
|
||||
|
||||
@ -13,16 +13,17 @@ import { theme } from "../terminal/theme.js";
|
||||
import { applyCliProfileEnv } from "./profile.js";
|
||||
import { seedWorkspaceFromAssets, type WorkspaceSeedResult } from "./workspace-seed.js";
|
||||
|
||||
const DEFAULT_IRONCLAW_PROFILE = "ironclaw";
|
||||
const IRONCLAW_STATE_DIRNAME = ".openclaw-ironclaw";
|
||||
const DEFAULT_DENCHCLAW_PROFILE = "dench";
|
||||
const DENCHCLAW_STATE_DIRNAME = ".openclaw-dench";
|
||||
const DEFAULT_GATEWAY_PORT = 18789;
|
||||
const IRONCLAW_GATEWAY_PORT_START = 19001;
|
||||
const DENCHCLAW_GATEWAY_PORT_START = 19001;
|
||||
const MAX_PORT_SCAN_ATTEMPTS = 100;
|
||||
const DEFAULT_WEB_APP_PORT = 3100;
|
||||
const WEB_APP_PROBE_ATTEMPTS = 20;
|
||||
const WEB_APP_PROBE_DELAY_MS = 750;
|
||||
const DEFAULT_BOOTSTRAP_ROLLOUT_STAGE = "default";
|
||||
const DEFAULT_GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway";
|
||||
const REQUIRED_TOOLS_PROFILE = "full";
|
||||
|
||||
type BootstrapRolloutStage = "internal" | "beta" | "default";
|
||||
type BootstrapCheckStatus = "pass" | "warn" | "fail";
|
||||
@ -267,13 +268,13 @@ export function resolveBootstrapRolloutStage(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): BootstrapRolloutStage {
|
||||
return normalizeBootstrapRolloutStage(
|
||||
env.IRONCLAW_BOOTSTRAP_ROLLOUT ?? env.OPENCLAW_BOOTSTRAP_ROLLOUT,
|
||||
env.DENCHCLAW_BOOTSTRAP_ROLLOUT ?? env.OPENCLAW_BOOTSTRAP_ROLLOUT,
|
||||
);
|
||||
}
|
||||
|
||||
export function isLegacyFallbackEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return (
|
||||
isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_LEGACY_FALLBACK) ||
|
||||
isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK) ||
|
||||
isTruthyEnvValue(env.OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK)
|
||||
);
|
||||
}
|
||||
@ -302,7 +303,7 @@ function firstNonEmptyLine(...values: Array<string | undefined>): string | undef
|
||||
function resolveProfileStateDir(profile: string, env: NodeJS.ProcessEnv = process.env): string {
|
||||
void profile;
|
||||
const home = resolveRequiredHomeDir(env, os.homedir);
|
||||
return path.join(home, IRONCLAW_STATE_DIRNAME);
|
||||
return path.join(home, DENCHCLAW_STATE_DIRNAME);
|
||||
}
|
||||
|
||||
function resolveGatewayLaunchAgentLabel(profile: string): string {
|
||||
@ -376,6 +377,15 @@ async function ensureSubagentDefaults(openclawCommand: string, profile: string):
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureToolsProfile(openclawCommand: string, profile: string): Promise<void> {
|
||||
await runOpenClawOrThrow({
|
||||
openclawCommand,
|
||||
args: ["--profile", profile, "config", "set", "tools.profile", REQUIRED_TOOLS_PROFILE],
|
||||
timeoutMs: 10_000,
|
||||
errorMessage: `Failed to set tools.profile=${REQUIRED_TOOLS_PROFILE}.`,
|
||||
});
|
||||
}
|
||||
|
||||
async function probeForWebApp(port: number): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 1_500);
|
||||
@ -923,11 +933,11 @@ function remediationForGatewayFailure(
|
||||
if (normalized.includes("address already in use") || normalized.includes("eaddrinuse")) {
|
||||
return `Port ${port} is busy. The bootstrap will auto-assign an available port, or you can explicitly specify one with \`--gateway-port <port>\`.`;
|
||||
}
|
||||
return `Run \`openclaw --profile ${profile} doctor --fix\` and retry \`ironclaw bootstrap --profile ${profile} --force-onboard\`.`;
|
||||
return `Run \`openclaw --profile ${profile} doctor --fix\` and retry \`denchclaw bootstrap --profile ${profile} --force-onboard\`.`;
|
||||
}
|
||||
|
||||
function remediationForWebUiFailure(port: number): string {
|
||||
return `Web UI did not respond on ${port}. Ensure the apps/web directory exists and rerun with \`ironclaw bootstrap --web-port <port>\` if needed.`;
|
||||
return `Web UI did not respond on ${port}. Ensure the apps/web directory exists and rerun with \`denchclaw bootstrap --web-port <port>\` if needed.`;
|
||||
}
|
||||
|
||||
function describeWorkspaceSeedResult(result: WorkspaceSeedResult): string {
|
||||
@ -1077,15 +1087,15 @@ export function buildBootstrapDiagnostics(params: {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.profile === DEFAULT_IRONCLAW_PROFILE) {
|
||||
if (params.profile === DEFAULT_DENCHCLAW_PROFILE) {
|
||||
checks.push(createCheck("profile", "pass", `Profile pinned: ${params.profile}.`));
|
||||
} else {
|
||||
checks.push(
|
||||
createCheck(
|
||||
"profile",
|
||||
"fail",
|
||||
`Ironclaw profile drift detected (${params.profile}).`,
|
||||
`Ironclaw requires \`--profile ${DEFAULT_IRONCLAW_PROFILE}\`. Re-run bootstrap to repair environment defaults.`,
|
||||
`DenchClaw profile drift detected (${params.profile}).`,
|
||||
`DenchClaw requires \`--profile ${DEFAULT_DENCHCLAW_PROFILE}\`. Re-run bootstrap to repair environment defaults.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1118,7 +1128,7 @@ export function buildBootstrapDiagnostics(params: {
|
||||
"agent-auth",
|
||||
"fail",
|
||||
authCheck.detail,
|
||||
`Run \`openclaw --profile ${DEFAULT_IRONCLAW_PROFILE} onboard --install-daemon\` to configure API keys.`,
|
||||
`Run \`openclaw --profile ${DEFAULT_DENCHCLAW_PROFILE} onboard --install-daemon\` to configure API keys.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1136,7 +1146,7 @@ export function buildBootstrapDiagnostics(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const expectedStateDir = resolveProfileStateDir(DEFAULT_IRONCLAW_PROFILE, env);
|
||||
const expectedStateDir = resolveProfileStateDir(DEFAULT_DENCHCLAW_PROFILE, env);
|
||||
const usesPinnedStateDir = path.resolve(stateDir) === path.resolve(expectedStateDir);
|
||||
if (usesPinnedStateDir) {
|
||||
checks.push(createCheck("state-isolation", "pass", `State dir pinned: ${stateDir}.`));
|
||||
@ -1146,13 +1156,13 @@ export function buildBootstrapDiagnostics(params: {
|
||||
"state-isolation",
|
||||
"fail",
|
||||
`Unexpected state dir: ${stateDir}.`,
|
||||
`Ironclaw requires \`${expectedStateDir}\`. Re-run bootstrap to restore pinned defaults.`,
|
||||
`DenchClaw requires \`${expectedStateDir}\`. Re-run bootstrap to restore pinned defaults.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const launchAgentLabel = resolveGatewayLaunchAgentLabel(params.profile);
|
||||
const expectedLaunchAgentLabel = resolveGatewayLaunchAgentLabel(DEFAULT_IRONCLAW_PROFILE);
|
||||
const expectedLaunchAgentLabel = resolveGatewayLaunchAgentLabel(DEFAULT_DENCHCLAW_PROFILE);
|
||||
if (launchAgentLabel === expectedLaunchAgentLabel) {
|
||||
checks.push(createCheck("daemon-label", "pass", `Gateway service label: ${launchAgentLabel}.`));
|
||||
} else {
|
||||
@ -1161,7 +1171,7 @@ export function buildBootstrapDiagnostics(params: {
|
||||
"daemon-label",
|
||||
"fail",
|
||||
`Gateway service label mismatch (${launchAgentLabel}).`,
|
||||
`Ironclaw requires launch agent label ${expectedLaunchAgentLabel}.`,
|
||||
`DenchClaw requires launch agent label ${expectedLaunchAgentLabel}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1172,14 +1182,14 @@ export function buildBootstrapDiagnostics(params: {
|
||||
params.rolloutStage === "default" ? "pass" : "warn",
|
||||
`Bootstrap rollout stage: ${params.rolloutStage}${params.legacyFallbackEnabled ? " (legacy fallback enabled)" : ""}.`,
|
||||
params.rolloutStage === "beta"
|
||||
? "Enable beta cutover by setting IRONCLAW_BOOTSTRAP_BETA_OPT_IN=1."
|
||||
? "Enable beta cutover by setting DENCHCLAW_BOOTSTRAP_BETA_OPT_IN=1."
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
const migrationSuiteOk = isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_MIGRATION_SUITE_OK);
|
||||
const onboardingE2EOk = isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_ONBOARDING_E2E_OK);
|
||||
const enforceCutoverGates = isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES);
|
||||
const migrationSuiteOk = isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_MIGRATION_SUITE_OK);
|
||||
const onboardingE2EOk = isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_ONBOARDING_E2E_OK);
|
||||
const enforceCutoverGates = isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES);
|
||||
const cutoverGatePassed = migrationSuiteOk && onboardingE2EOk;
|
||||
checks.push(
|
||||
createCheck(
|
||||
@ -1188,7 +1198,7 @@ export function buildBootstrapDiagnostics(params: {
|
||||
`Cutover gate: migrationSuite=${migrationSuiteOk ? "pass" : "missing"}, onboardingE2E=${onboardingE2EOk ? "pass" : "missing"}.`,
|
||||
cutoverGatePassed
|
||||
? undefined
|
||||
: "Run migration contracts + onboarding E2E and set IRONCLAW_BOOTSTRAP_MIGRATION_SUITE_OK=1 and IRONCLAW_BOOTSTRAP_ONBOARDING_E2E_OK=1 before full cutover.",
|
||||
: "Run migration contracts + onboarding E2E and set DENCHCLAW_BOOTSTRAP_MIGRATION_SUITE_OK=1 and DENCHCLAW_BOOTSTRAP_ONBOARDING_E2E_OK=1 before full cutover.",
|
||||
),
|
||||
);
|
||||
|
||||
@ -1297,14 +1307,14 @@ export async function bootstrapCommand(
|
||||
} else if (await isPortAvailable(DEFAULT_GATEWAY_PORT)) {
|
||||
gatewayPort = DEFAULT_GATEWAY_PORT;
|
||||
} else {
|
||||
// Default port is taken, find an available one starting from Ironclaw range
|
||||
// Default port is taken, find an available one starting from DenchClaw range
|
||||
const availablePort = await findAvailablePort(
|
||||
IRONCLAW_GATEWAY_PORT_START,
|
||||
DENCHCLAW_GATEWAY_PORT_START,
|
||||
MAX_PORT_SCAN_ATTEMPTS,
|
||||
);
|
||||
if (!availablePort) {
|
||||
throw new Error(
|
||||
`Could not find an available gateway port between ${IRONCLAW_GATEWAY_PORT_START} and ${IRONCLAW_GATEWAY_PORT_START + MAX_PORT_SCAN_ATTEMPTS}. ` +
|
||||
`Could not find an available gateway port between ${DENCHCLAW_GATEWAY_PORT_START} and ${DENCHCLAW_GATEWAY_PORT_START + MAX_PORT_SCAN_ATTEMPTS}. ` +
|
||||
`Please specify a port explicitly with --gateway-port.`,
|
||||
);
|
||||
}
|
||||
@ -1373,6 +1383,9 @@ export async function bootstrapCommand(
|
||||
// Persist the assigned port so all runtime clients (including web) resolve
|
||||
// the same gateway target on subsequent requests.
|
||||
await ensureGatewayPort(openclawCommand, profile, gatewayPort);
|
||||
// DenchClaw requires the full tool profile; onboarding defaults can drift to
|
||||
// messaging-only, so enforce this on every bootstrap run.
|
||||
await ensureToolsProfile(openclawCommand, profile);
|
||||
|
||||
await ensureSubagentDefaults(openclawCommand, profile);
|
||||
|
||||
@ -1476,7 +1489,7 @@ export async function bootstrapCommand(
|
||||
}
|
||||
logBootstrapChecklist(diagnostics, runtime);
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("IronClaw ready"));
|
||||
runtime.log(theme.heading("DenchClaw ready"));
|
||||
runtime.log(`Profile: ${profile}`);
|
||||
runtime.log(`OpenClaw CLI: ${installResult.version ?? "detected"}`);
|
||||
runtime.log(`Gateway: ${gatewayProbe.ok ? "reachable" : "check failed"}`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user