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:
parent
8b73cd45c0
commit
b78b67aedf
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user