agents: add self_update tool for non-owner gateway updates

This commit is contained in:
kumarabhirup 2026-02-22 00:38:18 -08:00
parent e5a3b3685f
commit 6e2f34e193
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
6 changed files with 132 additions and 10 deletions

View File

@ -164,3 +164,38 @@ describe("gateway tool", () => {
}
});
});
describe("self_update tool", () => {
it("is not owner-only", () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "self_update");
expect(tool).toBeDefined();
expect(tool?.ownerOnly).toBeUndefined();
});
it("calls update.run on the gateway", async () => {
const gateway = await import("./tools/gateway.js");
const callSpy = gateway.callGatewayTool as ReturnType<typeof vi.fn>;
callSpy.mockClear();
const tool = createOpenClawTools({
agentSessionKey: "agent:main:web:session1",
}).find((candidate) => candidate.name === "self_update");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing self_update tool");
}
await tool.execute("call-su-1", {
note: "user requested update",
});
expect(callSpy).toHaveBeenCalledWith(
"update.run",
expect.any(Object),
expect.objectContaining({
note: "user requested update",
sessionKey: "agent:main:web:session1",
}),
);
});
});

View File

@ -12,6 +12,7 @@ import { createGatewayTool } from "./tools/gateway-tool.js";
import { createImageTool } from "./tools/image-tool.js";
import { createMessageTool } from "./tools/message-tool.js";
import { createNodesTool } from "./tools/nodes-tool.js";
import { createSelfUpdateTool } from "./tools/self-update-tool.js";
import { createSessionStatusTool } from "./tools/session-status-tool.js";
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
@ -124,6 +125,9 @@ export function createOpenClawTools(options?: {
agentSessionKey: options?.agentSessionKey,
config: options?.config,
}),
createSelfUpdateTool({
agentSessionKey: options?.agentSessionKey,
}),
createAgentsListTool({
agentSessionKey: options?.agentSessionKey,
requesterAgentIdOverride: options?.requesterAgentIdOverride,

View File

@ -349,15 +349,24 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("- Opus: anthropic/claude-opus-4-5");
});
it("adds ClaudeBot self-update guidance when gateway tool is available", () => {
it("adds self-update guidance when gateway tool is available", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["gateway", "exec"],
});
expect(prompt).toContain("## OpenClaw Self-Update");
expect(prompt).toContain("## Ironclaw Self-Update");
expect(prompt).toContain("config.apply");
expect(prompt).toContain("update.run");
});
it("adds self-update guidance when self_update tool is available", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
toolNames: ["self_update", "exec"],
});
expect(prompt).toContain("## Ironclaw Self-Update");
expect(prompt).toContain("self_update");
});
it("includes skills guidance when skills prompt is present", () => {

View File

@ -328,6 +328,7 @@ export function buildAgentSystemPrompt(params: {
cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
message: "Send messages and channel actions",
gateway: "Restart, apply config, or run updates on the running OpenClaw process",
self_update: "Update Ironclaw to the latest version and restart the gateway",
agents_list: "List agent ids allowed for sessions_spawn",
sessions_list: "List other sessions (incl. sub-agents) with filters/last",
sessions_history: "Fetch history for another session/sub-agent",
@ -357,6 +358,7 @@ export function buildAgentSystemPrompt(params: {
"cron",
"message",
"gateway",
"self_update",
"agents_list",
"sessions_list",
"sessions_history",
@ -405,6 +407,7 @@ export function buildAgentSystemPrompt(params: {
}
const hasGateway = availableTools.has("gateway");
const hasSelfUpdate = availableTools.has("self_update");
const readToolName = resolveToolName("read");
const execToolName = resolveToolName("exec");
const processToolName = resolveToolName("process");
@ -538,16 +541,23 @@ export function buildAgentSystemPrompt(params: {
...skillsSection,
...memorySection,
// Skip self-update for subagent/none modes
hasGateway && !isMinimal ? "## OpenClaw Self-Update" : "",
hasGateway && !isMinimal
(hasGateway || hasSelfUpdate) && !isMinimal ? "## Ironclaw Self-Update" : "",
(hasGateway || hasSelfUpdate) && !isMinimal
? [
"Get Updates (self-update) is ONLY allowed when the user explicitly asks for it.",
'Self-update is ONLY allowed when the user explicitly asks for it (e.g. "update yourself", "upgrade", "get latest version").',
hasSelfUpdate
? "When the user asks to update, use the `self_update` tool. It updates the Ironclaw package and restarts the gateway automatically."
: "",
hasGateway
? "The `gateway` tool (owner-only) also supports update.run, config.apply, config.patch, and restart actions."
: "",
"Do not run config.apply or update.run unless the user explicitly requests an update or config change; if it's not explicit, ask first.",
"Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart).",
"After restart, OpenClaw pings the last active session automatically.",
].join("\n")
]
.filter(Boolean)
.join("\n")
: "",
hasGateway && !isMinimal ? "" : "",
(hasGateway || hasSelfUpdate) && !isMinimal ? "" : "",
"",
// Skip model aliases for subagent/none modes
params.modelAliasLines && params.modelAliasLines.length > 0 && !isMinimal

View File

@ -45,7 +45,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
// UI helpers
"group:ui": ["browser", "canvas"],
// Automation + infra
"group:automation": ["cron", "gateway"],
"group:automation": ["cron", "gateway", "self_update"],
// Messaging surface
"group:messaging": ["message"],
// Nodes + device tools
@ -58,6 +58,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
"cron",
"message",
"gateway",
"self_update",
"agents_list",
"sessions_list",
"sessions_history",

View File

@ -0,0 +1,63 @@
import { Type } from "@sinclair/typebox";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { type AnyAgentTool, jsonResult } from "./common.js";
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
const log = createSubsystemLogger("self-update-tool");
const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000;
const SelfUpdateToolSchema = Type.Object({
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
sessionKey: Type.Optional(Type.String()),
note: Type.Optional(Type.String()),
restartDelayMs: Type.Optional(Type.Number()),
});
/**
* A non-owner-only tool that only triggers `update.run` on the gateway.
* Unlike the full `gateway` tool (which exposes config mutation and restart),
* this tool is safe for any authorized sender.
*/
export function createSelfUpdateTool(opts?: { agentSessionKey?: string }): AnyAgentTool {
return {
label: "Self Update",
name: "self_update",
description:
"Update Ironclaw to the latest version and restart the gateway. Use when the user asks to update, upgrade, or get the latest version.",
parameters: SelfUpdateToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const gatewayOpts = readGatewayCallOptions(params);
const sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()
? params.sessionKey.trim()
: opts?.agentSessionKey?.trim() || undefined;
const note =
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
const restartDelayMs =
typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs)
? Math.floor(params.restartDelayMs)
: undefined;
const updateTimeoutMs = gatewayOpts.timeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
const updateGatewayOpts = {
...gatewayOpts,
timeoutMs: updateTimeoutMs,
};
log.info(`self_update tool: update requested (sessionKey=${sessionKey ?? "none"})`);
const result = await callGatewayTool("update.run", updateGatewayOpts, {
sessionKey,
note,
restartDelayMs,
timeoutMs: updateTimeoutMs,
});
return jsonResult({ ok: true, result });
},
};
}