Isolate IM cron tools and trust-gate read APIs.

Scope list/pause/delete to the calling chat, bind IM delivery to that peer, ignore client job ids, require trusted Host for GETs, and wrap mirrored summaries as system reminders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-06 14:45:37 +08:00
parent 29d946f3dd
commit 67238d746e
7 changed files with 222 additions and 29 deletions

View file

@ -149,9 +149,7 @@ export function matchTargetForPeer(targets, peer) {
const jid = String(route.jid || route.chatId || route.openId || route.userId || '').trim().toLowerCase()
if (!jid) continue
if (wantGroup && !jid.endsWith('@g.us')) continue
if (candidates.some((peerJid) => (
jid === peerJid || peerJid.endsWith(jid) || jid.endsWith(peerJid)
))) {
if (candidates.some((peerJid) => jid === peerJid)) {
return target
}
}
@ -365,25 +363,64 @@ export async function resolveCreateDelivery(args = {}, exec = {}, deps = {}) {
const targetId = typeof args.im_target_id === 'string' ? args.im_target_id.trim()
: (typeof args.imTargetId === 'string' ? args.imTargetId.trim() : '')
const dshIm = deps.dshIm
const peer = await resolveCallerPeer(exec, dshIm)
// IM peers cannot retarget to an arbitrary catalog entry (confused deputy).
if (peer?.botId) {
if (explicit === 'dsh') return normalizeDelivery({ kind: 'dsh' })
const ensured = await ensureImDeliveryForPeer(peer, dshIm)
if (ensured) return ensured
if (explicit === 'im' || (botId && targetId)) {
const error = new Error('IM sessions cannot pick a free im_bot_id/im_target_id; delivery is bound to this chat')
error.code = 'IM_TARGET_FORBIDDEN'
throw error
}
return normalizeDelivery({ kind: 'dsh' })
}
if (explicit === 'dsh') return normalizeDelivery({ kind: 'dsh' })
if (explicit === 'im' || (botId && targetId)) {
return normalizeDelivery({ kind: 'im', botId, targetId })
}
const dshIm = deps.dshIm
const sessionId = callerSessionId(exec)
if (sessionId && dshIm && typeof dshIm.resolveSessionPeer === 'function') {
try {
const peer = await dshIm.resolveSessionPeer(sessionId)
const ensured = await ensureImDeliveryForPeer(peer, dshIm)
if (ensured) return ensured
} catch {
// fall through to dsh
}
}
return normalizeDelivery({ kind: 'dsh' })
}
/**
* Resolve the IM peer for the tool-calling session, if any.
* @param {object} exec
* @param {object|undefined} dshIm
*/
export async function resolveCallerPeer(exec, dshIm) {
const sessionId = callerSessionId(exec)
if (!sessionId || !dshIm || typeof dshIm.resolveSessionPeer !== 'function') return null
try {
const peer = await dshIm.resolveSessionPeer(sessionId)
return peer?.botId ? peer : null
} catch {
return null
}
}
/**
* Whether a scheduled job was created from this IM conversation.
* Web/sidebar callers are unrestricted; IM peers only manage their own jobs.
* @param {object|null|undefined} job
* @param {object} peer
*/
export function jobVisibleToPeer(job, peer) {
if (!job || !peer?.botId) return false
const botId = String(peer.botId).trim()
const conversationKey = String(peer.conversationKey || '').trim()
const conversationId = String(peer.conversationId || '').trim()
const origin = normalizeOrigin(job.origin)
if (origin?.kind !== 'im' || origin.peer?.botId !== botId) return false
if (conversationKey && origin.peer.conversationKey === conversationKey) return true
if (conversationId && origin.peer.conversationId === conversationId) return true
return false
}
const IM_MAX_CHARS = 3500
/**
@ -522,14 +559,17 @@ export async function mirrorRunToSession(job, summary, deps = {}) {
}
const text = formatRunResultBody(job, summary)
const safe = text.replaceAll('</system-reminder>', '<\\/system-reminder>')
const plugin = typeof deps.pluginName === 'string' && deps.pluginName.trim()
? deps.pluginName.trim()
: 'dsh-ops-cron'
const id = typeof deps.newId === 'function' ? deps.newId() : `cron-mirror-${Date.now()}`
// Keep role=user + user/message (assistant/message requires model source + open turn).
// Wrap as system-reminder so the next model turn does not treat cron output as user intent.
const message = {
id,
role: 'user',
content: [{ type: 'text', text }],
content: [{ type: 'text', text: `<system-reminder>\n${safe}\n</system-reminder>` }],
source: { kind: 'plugin', plugin },
}