From f61896b03cc7031f51106a04566831f4ac2a0bd7 Mon Sep 17 00:00:00 2001 From: Devin Robison Date: Fri, 17 Apr 2026 12:43:48 -0600 Subject: [PATCH] fix(cron): preserve untrusted awareness event labels (#68210) * fix(cron): preserve untrusted awareness event labels Keep isolated cron awareness summaries untrusted when they are promoted into the main session, and forward explicit trust downgrades through the gateway cron wrapper. Add focused regression coverage for both paths. * changelog: note cron awareness untrusted-label preservation (#68210) --- CHANGELOG.md | 1 + .../delivery-dispatch.double-announce.test.ts | 1 + src/cron/isolated-agent/delivery-dispatch.ts | 1 + src/cron/service/state.ts | 2 +- src/gateway/server-cron.test.ts | 41 +++++++++++++++++++ src/gateway/server-cron.ts | 6 ++- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be6957c430f..cab361b79e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai - Models status/OAuth health: align OAuth health reporting with the same effective credential view runtime uses, so expired refreshable sessions stop showing healthy by default and fresher imported Codex CLI credentials surface correctly in `models status`, doctor, and gateway auth status. Thanks @vincentkoc. - Twitch/setup: load Twitch through the bundled setup-entry discovery path and keep setup/status account detection aligned with runtime config. (#68008) Thanks @gumadeiras. - Feishu/card actions: resolve card-action chat type from the Feishu chat API when stored context is missing, preferring `chat_mode` over `chat_type`, so DM-originated card actions no longer bypass `dmPolicy` by falling through to the group handling path. (#68201) +- Cron/isolated-agent: preserve `trusted: false` on isolated cron awareness events mirrored into the main session, and forward the optional `trusted` flag through the gateway cron wrapper so explicit trust downgrades survive session-key scoping. (#68210) ## 2026.4.15 diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index 52dc888b592..f3caf6f7428 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -324,6 +324,7 @@ describe("dispatchCronDelivery — double-announce guard", () => { expect(enqueueSystemEvent).toHaveBeenCalledWith("Morning briefing complete.", { sessionKey: "agent:main:main", contextKey: "cron-direct-delivery:v1:run-123:telegram::123456:", + trusted: false, }); }); diff --git a/src/cron/isolated-agent/delivery-dispatch.ts b/src/cron/isolated-agent/delivery-dispatch.ts index 687bebfbac8..c53a6fca0c6 100644 --- a/src/cron/isolated-agent/delivery-dispatch.ts +++ b/src/cron/isolated-agent/delivery-dispatch.ts @@ -351,6 +351,7 @@ async function queueCronAwarenessSystemEvent(params: { agentId: params.agentId, }), contextKey: params.deliveryIdempotencyKey, + trusted: false, }); } catch (err) { await logCronDeliveryWarn( diff --git a/src/cron/service/state.ts b/src/cron/service/state.ts index 073efd8f459..f57073fbf0e 100644 --- a/src/cron/service/state.ts +++ b/src/cron/service/state.ts @@ -62,7 +62,7 @@ export type CronServiceDeps = { maxMissedJobsPerRestart?: number; enqueueSystemEvent: ( text: string, - opts?: { agentId?: string; sessionKey?: string; contextKey?: string }, + opts?: { agentId?: string; sessionKey?: string; contextKey?: string; trusted?: boolean }, ) => void; requestHeartbeatNow: (opts?: { reason?: string; agentId?: string; sessionKey?: string }) => void; runHeartbeatOnce?: (opts?: { diff --git a/src/gateway/server-cron.test.ts b/src/gateway/server-cron.test.ts index 28e048732de..c28a8f2bd41 100644 --- a/src/gateway/server-cron.test.ts +++ b/src/gateway/server-cron.test.ts @@ -140,6 +140,47 @@ describe("buildGatewayCronService", () => { } }); + it("preserves trust downgrades when cron enqueues system events", () => { + const cfg = createCronConfig("server-cron-untrusted"); + loadConfigMock.mockReturnValue(cfg); + + const state = buildGatewayCronService({ + cfg, + deps: {} as CliDeps, + broadcast: () => {}, + }); + try { + const cronDeps = ( + state.cron as unknown as { + state?: { + deps?: { + enqueueSystemEvent?: (optsText: string, opts?: { + agentId?: string; + sessionKey?: string; + contextKey?: string; + trusted?: boolean; + }) => void; + }; + }; + } + ).state?.deps; + + cronDeps?.enqueueSystemEvent?.("hello", { + sessionKey: "discord:channel:ops", + contextKey: "cron:test", + trusted: false, + }); + + expect(enqueueSystemEventMock).toHaveBeenCalledWith("hello", { + sessionKey: "agent:main:discord:channel:ops", + contextKey: "cron:test", + trusted: false, + }); + } finally { + state.cron.stop(); + } + }); + it("blocks private webhook URLs via SSRF-guarded fetch", async () => { const cfg = createCronConfig("server-cron-ssrf"); loadConfigMock.mockReturnValue(cfg); diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 0d9b49a8600..f73289facd1 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -285,7 +285,11 @@ export function buildGatewayCronService(params: { agentId, requestedSessionKey: opts?.sessionKey, }); - enqueueSystemEvent(text, { sessionKey, contextKey: opts?.contextKey }); + enqueueSystemEvent(text, { + sessionKey, + contextKey: opts?.contextKey, + trusted: opts?.trusted, + }); }, requestHeartbeatNow: (opts) => { const { agentId, sessionKey } = resolveCronWakeTarget(opts);