openclaw - 💡(How to fix) Fix ForceSenderIsOwnerFalse=true for cron-fired announce delivery strips owner-only tools (cron, gateway) on next owner DM [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72954Fetched 2026-04-28 06:29:33
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Author
Participants

When an isolated cron job with delivery.mode: "announce" fires and delivers its output back into the originating Discord/Telegram DM session, the queued system event is hardcoded to trusted: false (src/cron/isolated-agent/delivery-dispatch.ts:359). On the next inbound DM from that owner, src/infra/heartbeat-runner.ts:984 sets ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEventssenderIsOwner=falseapplyOwnerOnlyToolPolicy strips cron, gateway, nodes from the model's tool inventory for that turn. The model then has no native cron primitive, falls back to exec, hallucinates a host CLI invocation (npx openclaw cron add ...) that fails in the sandbox, and then anchors on its own "I can't create cron from sandbox" assistant statement for several follow-up turns even after the platform state recovers.

Root Cause

When an isolated cron job with delivery.mode: "announce" fires and delivers its output back into the originating Discord/Telegram DM session, the queued system event is hardcoded to trusted: false (src/cron/isolated-agent/delivery-dispatch.ts:359). On the next inbound DM from that owner, src/infra/heartbeat-runner.ts:984 sets ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEventssenderIsOwner=falseapplyOwnerOnlyToolPolicy strips cron, gateway, nodes from the model's tool inventory for that turn. The model then has no native cron primitive, falls back to exec, hallucinates a host CLI invocation (npx openclaw cron add ...) that fails in the sandbox, and then anchors on its own "I can't create cron from sandbox" assistant statement for several follow-up turns even after the platform state recovers.

Fix Action

Fix / Workaround

When an isolated cron job with delivery.mode: "announce" fires and delivers its output back into the originating Discord/Telegram DM session, the queued system event is hardcoded to trusted: false (src/cron/isolated-agent/delivery-dispatch.ts:359). On the next inbound DM from that owner, src/infra/heartbeat-runner.ts:984 sets ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEventssenderIsOwner=falseapplyOwnerOnlyToolPolicy strips cron, gateway, nodes from the model's tool inventory for that turn. The model then has no native cron primitive, falls back to exec, hallucinates a host CLI invocation (npx openclaw cron add ...) that fails in the sandbox, and then anchors on its own "I can't create cron from sandbox" assistant statement for several follow-up turns even after the platform state recovers.

  1. Owner DMs the agent on Discord and asks for a daily cron (e.g. "set a cron at 16:19 UK time, send me a 4-line poem about Letterkenny").
  2. Agent calls native cron tool. successfulCronAdds increments. Job is created with payload.kind: "agentTurn", sessionTarget: "isolated", delivery.{channel: "discord", mode: "announce"}.
  3. Cron fires. Isolated subagent generates the poem. delivery-dispatch.ts:359 calls enqueueSystemEvent(text, { sessionKey: <main>, contextKey: <idempotency>, trusted: false }). The poem reaches Discord as a System (untrusted): ... prefix on the next message bundle.
  4. Owner sends another DM (any text) — say, "now add another cron for Dublin".
  5. heartbeat-runner.ts:984 constructs the ctx for that turn:
    ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEvents
    hasUntrustedPendingEvents=true (the queued cron poem hasn't been "consumed" by an owner-acknowledged turn yet) → ForceSenderIsOwnerFalse=true.
  6. command-auth.ts:696:
    const senderIsOwner = ctx.ForceSenderIsOwnerFalse
      ? false
      : senderIsOwnerByIdentity || senderIsOwnerByScope || ownerState.ownerAllowAll;
    short-circuits to false.
  7. applyOwnerOnlyToolPolicy strips cron and gateway from the model's tool schema array.
  8. Model receives the user's "add another cron" turn with no cron slot, falls back to exec, and produces host-CLI hallucinations.

Why the obvious config workarounds don't work

Code Example

ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEvents

---

const senderIsOwner = ctx.ForceSenderIsOwnerFalse
     ? false
     : senderIsOwnerByIdentity || senderIsOwnerByScope || ownerState.ownerAllowAll;
RAW_BUFFERClick to expand / collapse

Bug: ForceSenderIsOwnerFalse=true for cron-fired delivery.mode: "announce" strips owner-only tools (cron, gateway) for the next owner DM

Summary

When an isolated cron job with delivery.mode: "announce" fires and delivers its output back into the originating Discord/Telegram DM session, the queued system event is hardcoded to trusted: false (src/cron/isolated-agent/delivery-dispatch.ts:359). On the next inbound DM from that owner, src/infra/heartbeat-runner.ts:984 sets ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEventssenderIsOwner=falseapplyOwnerOnlyToolPolicy strips cron, gateway, nodes from the model's tool inventory for that turn. The model then has no native cron primitive, falls back to exec, hallucinates a host CLI invocation (npx openclaw cron add ...) that fails in the sandbox, and then anchors on its own "I can't create cron from sandbox" assistant statement for several follow-up turns even after the platform state recovers.

Reproduction (verified on [email protected], commit a979721)

  1. Owner DMs the agent on Discord and asks for a daily cron (e.g. "set a cron at 16:19 UK time, send me a 4-line poem about Letterkenny").
  2. Agent calls native cron tool. successfulCronAdds increments. Job is created with payload.kind: "agentTurn", sessionTarget: "isolated", delivery.{channel: "discord", mode: "announce"}.
  3. Cron fires. Isolated subagent generates the poem. delivery-dispatch.ts:359 calls enqueueSystemEvent(text, { sessionKey: <main>, contextKey: <idempotency>, trusted: false }). The poem reaches Discord as a System (untrusted): ... prefix on the next message bundle.
  4. Owner sends another DM (any text) — say, "now add another cron for Dublin".
  5. heartbeat-runner.ts:984 constructs the ctx for that turn:
    ForceSenderIsOwnerFalse: hasExecCompletion || hasUntrustedPendingEvents
    hasUntrustedPendingEvents=true (the queued cron poem hasn't been "consumed" by an owner-acknowledged turn yet) → ForceSenderIsOwnerFalse=true.
  6. command-auth.ts:696:
    const senderIsOwner = ctx.ForceSenderIsOwnerFalse
      ? false
      : senderIsOwnerByIdentity || senderIsOwnerByScope || ownerState.ownerAllowAll;
    short-circuits to false.
  7. applyOwnerOnlyToolPolicy strips cron and gateway from the model's tool schema array.
  8. Model receives the user's "add another cron" turn with no cron slot, falls back to exec, and produces host-CLI hallucinations.

Trace evidence

Same persistent main session (agent:rick:main, sessionId b9a549d6-a003-4f41-949b-4ab4cde3146c), three context.compiled events on 2026-04-27:

EventUTCtools.entries includes crontools.entries includes gatewayOutcome
First cron-set request (clean)15:16:41yesyessuccessfulCronAdds=1, "Готово!"
Second request, after cron fired and announce delivered16:10:51nonomodel: "не могу из sandbox", exec only
Retry 76s later (next turn, same conversation)16:12:06yesyestools restored — but model anchored on its own prior "I can't" message and continued exec hallucinations

prompt.submitted payload at 16:10:51 contains the user's message with valid sender_id: "927969257684361256" and the prepended System (untrusted): [poem text]. Sender (untrusted metadata).id matches commands.ownerAllowFrom ("discord:927...") and accounts.default.dm.allowFrom exactly. Owner-by-identity would match — but ForceSenderIsOwnerFalse short-circuits before the match check runs.

Why the obvious config workarounds don't work

  • commands.ownerAllowFrom (with or without discord: prefix) — not consulted, ForceSenderIsOwnerFalse is checked before owner resolution.
  • Per-channel heartbeat.{showAlerts, showOk, useIndicator} — the ForceSenderIsOwnerFalse ctx field is built unconditionally at line 984 before the visibility skip-path at line 986.
  • delivery.mode: "webhook" — works for non-DM destinations, but doesn't fit the "deliver the agent-generated poem to the owner's chat" use case.
  • payload.kind: "systemEvent" + sessionTarget: "main" — works for static text, but loses the agent-generated content (which is the whole point of agentTurn jobs).

Suggested fixes (any of these would resolve)

  1. Trust the originating cron job's authorship. When delivery-dispatch.ts:359 enqueues the system event, set trusted: <boolean> based on whether the cron job itself was created by an owner (which is already enforced — cron.add is in owner-only-tools.ts). Owner-created cron output is by definition owner-trusted — there's no untrusted source.
  2. Don't override senderIsOwner when the inbound message has a valid owner-matching SenderId. In command-auth.ts:696, treat ForceSenderIsOwnerFalse as a hint for "no acknowledged owner ctx" rather than a hard override; if senderIsOwnerByIdentity would otherwise be true, prefer that.
  3. Consume the pending event before evaluating hasUntrustedPendingEvents. Move consumeSystemEventEntries(...) ahead of ctx construction so a subsequent owner DM doesn't see lingering "pending" state from a cron poem that was already delivered.

(1) is the smallest change and the most semantically correct — cron output is exactly as trusted as the cron job's creator. Happy to send a PR if you'd like.

Why this matters for downstream

In our deployment (Amplify B2C, multi-tenant OpenClaw fleet on 2026.4.23), every per-client agent that uses agentTurn-payload cron jobs with announce delivery is exposed to this on every cron fire. We've shipped a config-side fix at our side (https://github.com/fychmen/amplify/pull/728) that ensures the four owner-by-identity invariants are correct in channels.discord.{allowFrom, dm.allowFrom, accounts.default.{allowFrom, dm.{enabled,allowFrom}}} so the resolution chain works the moment ForceSenderIsOwnerFalse isn't set, but the flicker recurs on every cron-fired announce until upstream fixes the trusted: false hardcode or the heartbeat-ctx override.

Environment

  • openclaw 2026.4.23 (a979721)
  • Node 22.22.2, Linux 6.8
  • Channel: Discord (@openclaw first-party plugin via extensions/discord)
  • Model: xiaomi/mimo-v2.5-pro via OpenRouter (also reproduced on claude-sonnet-4.6)
  • Cron jobs path: /home/<client>/.openclaw/cron/jobs.json
  • Trajectory file demonstrating the flicker: available on request

extent analysis

TL;DR

Set trusted: true when enqueuing system events for cron job output to prevent overriding senderIsOwner and stripping owner-only tools.

Guidance

  • Identify the source of the trusted: false setting in delivery-dispatch.ts:359 and consider setting it to true for cron job output.
  • Review command-auth.ts:696 to ensure senderIsOwner is not overridden when ForceSenderIsOwnerFalse is true and senderIsOwnerByIdentity would otherwise be true.
  • Consider consuming pending system events before evaluating hasUntrustedPendingEvents to prevent lingering "pending" state.

Example

// delivery-dispatch.ts:359
enqueueSystemEvent(text, { sessionKey: <main>, contextKey: <idempotency>, trusted: true });

Notes

This fix assumes that cron job output is trusted if the job was created by an owner. Additional validation may be necessary to ensure the security of this approach.

Recommendation

Apply workaround (1) by trusting the originating cron job's authorship when enqueuing system events, as it is the smallest change and most semantically correct.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix ForceSenderIsOwnerFalse=true for cron-fired announce delivery strips owner-only tools (cron, gateway) on next owner DM [1 participants]