mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 03:03:18 +08:00
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:
parent
29d946f3dd
commit
67238d746e
7 changed files with 222 additions and 29 deletions
|
|
@ -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 },
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue