feat(bootstrap): enable elevated commands for webchat on first boot (#113)

Stages elevated tooling config pre-onboard and reapplies via CLI post-onboard so webchat gets host exec from first boot.
This commit is contained in:
Kumar Abhirup 2026-03-20 13:13:46 -07:00 committed by GitHub
parent 8b73cd45c0
commit b78b67aedf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 175 additions and 1 deletions

View File

@ -1,6 +1,6 @@
{
"name": "denchclaw",
"version": "2.3.17",
"version": "2.3.18",
"description": "Fully Managed OpenClaw Framework for managing your CRM, Sales Automation and Outreach agents. The only local productivity tool you need.",
"keywords": [],
"homepage": "https://github.com/DenchHQ/DenchClaw#readme",

View File

@ -1722,6 +1722,159 @@ describe("bootstrapCommand always-onboard behavior", () => {
expect(logMessages).toContain("gateway.err.log");
});
it("stages elevated commands config in raw JSON before onboard (webchat gets host exec from first boot)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const configPath = path.join(stateDir, "openclaw.json");
const config = JSON.parse(readFileSync(configPath, "utf-8"));
expect(config.tools?.elevated?.enabled).toBe(true);
expect(config.tools?.elevated?.allowFrom?.webchat).toEqual(["*"]);
expect(config.commands?.bash).toBe(true);
expect(config.commands?.config).toBe(true);
expect(config.agents?.defaults?.elevatedDefault).toBe("on");
const onboardIndex = spawnCalls.findIndex(
(c) => c.command === "openclaw" && c.args.includes("onboard"),
);
const preOnboardElevatedCliSet = spawnCalls.findIndex((call, index) => {
return (
index < onboardIndex &&
call.command === "openclaw" &&
call.args.includes("config") &&
call.args.includes("set") &&
call.args.includes("tools.elevated.enabled")
);
});
expect(preOnboardElevatedCliSet).toBe(-1);
});
it("applies elevated commands via CLI after onboard (prevents onboard wizard drift)", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const onboardIndex = spawnCalls.findIndex(
(call) => call.command === "openclaw" && call.args.includes("onboard"),
);
expect(onboardIndex).toBeGreaterThan(-1);
const elevatedSettings = [
{ key: "tools.elevated.enabled", value: "true" },
{ key: "tools.elevated.allowFrom.webchat", value: '["*"]' },
{ key: "agents.defaults.elevatedDefault", value: "on" },
{ key: "commands.bash", value: "true" },
{ key: "commands.config", value: "true" },
];
for (const { key, value } of elevatedSettings) {
const postOnboardSetCall = spawnCalls.find(
(call, index) =>
index > onboardIndex &&
call.command === "openclaw" &&
call.args.includes("config") &&
call.args.includes("set") &&
call.args.includes(key) &&
call.args.includes(value),
);
expect(postOnboardSetCall, `expected post-onboard config set for ${key}=${value}`).toBeDefined();
expect(postOnboardSetCall?.args).toEqual(
expect.arrayContaining(["--profile", "dench", "config", "set", key, value]),
);
}
});
it("reapplies elevated commands on repeated bootstrap runs (idempotent 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 elevatedEnabledCalls = spawnCalls.filter(
(call) =>
call.command === "openclaw" &&
call.args.includes("config") &&
call.args.includes("set") &&
call.args.includes("tools.elevated.enabled"),
);
expect(elevatedEnabledCalls).toHaveLength(2);
for (const call of elevatedEnabledCalls) {
expect(call.args).toEqual(
expect.arrayContaining(["--profile", "dench", "config", "set", "tools.elevated.enabled", "true"]),
);
}
});
it("preserves elevated config in final openclaw.json after full bootstrap cycle", async () => {
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
await bootstrapCommand(
{
nonInteractive: true,
noOpen: true,
skipUpdate: true,
},
runtime,
);
const configPath = path.join(stateDir, "openclaw.json");
const finalConfig = JSON.parse(readFileSync(configPath, "utf-8"));
expect(finalConfig.tools?.elevated?.enabled).toBe(true);
expect(finalConfig.tools?.elevated?.allowFrom?.webchat).toEqual(["*"]);
expect(finalConfig.agents?.defaults?.elevatedDefault).toBe("on");
expect(finalConfig.commands?.bash).toBe(true);
expect(finalConfig.commands?.config).toBe(true);
expect(finalConfig.agents?.defaults?.timeoutSeconds).toBe(86400);
expect(finalConfig.tools?.profile).toBe("full");
});
it("strips npm_config_* env vars from npm global commands (prevents npx prefix hijack)", async () => {
process.env.npm_config_prefix = "/tmp/npx-fake-prefix";
process.env.npm_config_global_prefix = "/tmp/npx-fake-global";

View File

@ -710,6 +710,22 @@ function stagePreOnboardConfig(
gateway.port = params.gatewayPort;
raw.gateway = gateway;
const tools = { ...(asRecord(raw.tools) ?? {}) };
const elevated = { ...(asRecord(tools.elevated) ?? {}) };
elevated.enabled = true;
const allowFrom = { ...(asRecord(elevated.allowFrom) ?? {}) };
allowFrom.webchat = ["*"];
elevated.allowFrom = allowFrom;
tools.elevated = elevated;
raw.tools = tools;
const commands = { ...(asRecord(raw.commands) ?? {}) };
commands.bash = true;
commands.config = true;
raw.commands = commands;
defaults.elevatedDefault = "on";
mkdirSync(stateDir, { recursive: true });
writeFileSync(
path.join(stateDir, "openclaw.json"),
@ -732,6 +748,11 @@ async function ensureAgentDefaults(openclawCommand: string, profile: string): Pr
["agents.defaults.subagents.archiveAfterMinutes", "180"],
["agents.defaults.subagents.runTimeoutSeconds", "0"],
["tools.subagents.tools.deny", "[]"],
["tools.elevated.enabled", "true"],
["tools.elevated.allowFrom.webchat", '["*"]'],
["agents.defaults.elevatedDefault", "on"],
["commands.bash", "true"],
["commands.config", "true"],
];
for (const [key, value] of settings) {
await runOpenClawOrThrow({