agents: add self_update tool for non-owner gateway updates
This commit is contained in:
parent
e5a3b3685f
commit
6e2f34e193
@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
63
src/agents/tools/self-update-tool.ts
Normal file
63
src/agents/tools/self-update-tool.ts
Normal 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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user