From a3fda2ada97b8ff8bf60368d111981f114751512 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Wed, 13 May 2026 07:24:41 +0530 Subject: [PATCH] Limit hook CLI tool authority [AI] (#81065) * fix: limit hook cli tool authority * docs: add changelog entry for PR merge --- CHANGELOG.md | 1 + src/cron/isolated-agent/run-executor.ts | 5 ++- .../run.message-tool-policy.test.ts | 1 + .../run.session-key-isolation.test.ts | 31 +++++++++++++++++++ src/cron/isolated-agent/run.ts | 3 ++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4b5961675..49d69fcc981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987. - Require admin scope for node device token management [AI]. (#81067) Thanks @pgondhi987. - Restrict chat sender allowlist matching [AI]. (#80898) Thanks @pgondhi987. - Sessions: redact persisted tool result detail metadata before writing transcripts so diagnostic secrets do not survive tool output redaction. (#80444) Thanks @nimbleenigma. diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 190f40a3f00..af23a7539d6 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -82,6 +82,7 @@ export function createCronPromptExecutor(params: { resolvedVerboseLevel: VerboseLevel; thinkLevel: ThinkLevel | undefined; timeoutMs: number; + senderIsOwner: boolean; messageChannel: string | undefined; suppressExecNotifyOnExit: boolean; resolvedDelivery: { @@ -177,7 +178,7 @@ export function createCronPromptExecutor(params: { onExecutionPhase: params.onExecutionPhase, bootstrapPromptWarningSignaturesSeen, bootstrapPromptWarningSignature, - senderIsOwner: true, + senderIsOwner: params.senderIsOwner, }); bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen( result.meta?.systemPromptReport, @@ -314,6 +315,7 @@ export async function executeCronRun(params: { ) => void; thinkLevel: ThinkLevel | undefined; timeoutMs: number; + senderIsOwner: boolean; suppressExecNotifyOnExit: boolean; runStartedAt?: number; }): Promise { @@ -350,6 +352,7 @@ export async function executeCronRun(params: { abortReason: params.abortReason, onExecutionStarted: params.onExecutionStarted, onExecutionPhase: params.onExecutionPhase, + senderIsOwner: params.senderIsOwner, }); const runStartedAt = params.runStartedAt ?? Date.now(); diff --git a/src/cron/isolated-agent/run.message-tool-policy.test.ts b/src/cron/isolated-agent/run.message-tool-policy.test.ts index 7215e357112..d2ee5c51f7c 100644 --- a/src/cron/isolated-agent/run.message-tool-policy.test.ts +++ b/src/cron/isolated-agent/run.message-tool-policy.test.ts @@ -319,6 +319,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolvedVerboseLevel: "off", thinkLevel: undefined, timeoutMs: 60_000, + senderIsOwner: true, messageChannel: "messagechat", suppressExecNotifyOnExit: true, toolPolicy: { diff --git a/src/cron/isolated-agent/run.session-key-isolation.test.ts b/src/cron/isolated-agent/run.session-key-isolation.test.ts index 3b707e48575..6edbd985387 100644 --- a/src/cron/isolated-agent/run.session-key-isolation.test.ts +++ b/src/cron/isolated-agent/run.session-key-isolation.test.ts @@ -121,9 +121,40 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => { const runRequest = requireFirstMockArg(runCliAgentMock, "runCliAgentMock") as { sessionId?: string; sessionKey?: string; + senderIsOwner?: boolean; }; expect(runRequest.sessionId).toBe("isolated-cli-run-1"); expect(runRequest.sessionKey).toBe("agent:default:cron:cli-monitor:run:isolated-cli-run-1"); expect(runRequest.sessionKey).not.toBe("agent:default:cron:cli-monitor"); + expect(runRequest.senderIsOwner).toBe(true); + }); + + it("runs externally sourced CLI hook turns without owner tool authority", async () => { + isCliProviderMock.mockReturnValue(true); + mockRunCronFallbackPassthrough(); + runCliAgentMock.mockResolvedValue({ + payloads: [{ text: "done" }], + meta: { agentMeta: { usage: { input: 10, output: 20 } } }, + }); + + const result = await runCronIsolatedAgentTurn( + makeIsolatedAgentTurnParams({ + sessionKey: "hook:webhook:cli-monitor", + job: makeIsolatedAgentTurnJob({ + payload: { + kind: "agentTurn", + message: "test", + externalContentSource: "webhook", + }, + }), + }), + ); + + expect(result.status).toBe("ok"); + expect(runCliAgentMock).toHaveBeenCalledOnce(); + const runRequest = requireFirstMockArg(runCliAgentMock, "runCliAgentMock") as { + senderIsOwner?: boolean; + }; + expect(runRequest.senderIsOwner).toBe(false); }); }); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index ee708020430..14f21d1a160 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -462,6 +462,7 @@ type PreparedCronRunContext = { resolvedDelivery: ResolvedCronDeliveryTarget; deliveryRequested: boolean; suppressExecNotifyOnExit: boolean; + senderIsOwner: boolean; toolPolicy: ReturnType; skillsSnapshot: SkillSnapshot; liveSelection: CronLiveSelection; @@ -792,6 +793,7 @@ async function prepareCronRunContext(params: { resolvedDelivery, deliveryRequested, suppressExecNotifyOnExit: deliveryPlan.mode === "none", + senderIsOwner: !isExternalHook, toolPolicy, skillsSnapshot, liveSelection, @@ -1145,6 +1147,7 @@ export async function runCronIsolatedAgentTurn(params: { thinkLevel: prepared.context.thinkLevel, timeoutMs: prepared.context.timeoutMs, suppressExecNotifyOnExit: prepared.context.suppressExecNotifyOnExit, + senderIsOwner: prepared.context.senderIsOwner, }); if (isAborted()) { return prepared.context.withRunSession({