Stamp and reclaim chat-created cron jobs for the current user.

Tool creates often left ownerEmpNo unassigned so the sidebar hid them while runs still fired; infer identity from session/cwd, claim legacy orphans on list, and keep cron rows visible without leaking run sessions into the workspace list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-08 16:19:59 +08:00
parent d298c77f3f
commit a0dc070372
7 changed files with 254 additions and 25 deletions

View file

@ -15,6 +15,7 @@ import { resolveCreateAgentPreset } from './preset.js'
import {
assertCanAccessJob,
filterJobsForIdentity,
jobVisibleToIdentity,
UNASSIGNED_OWNER,
} from './ownership.js'
@ -127,18 +128,67 @@ export function resolveCreateCwd(args, exec, { peer = null } = {}) {
return sessionCwd
}
function forbidForeignJob(job, peer) {
if (!peer?.botId) return job
if (jobVisibleToPeer(job, peer)) return job
const error = new Error('job not found or not owned by this chat')
error.code = 'NOT_FOUND'
throw error
function callerSessionId(exec) {
return String(
exec?.agent?.session?.id
|| exec?.agent?.session?.header?.id
|| exec?.sessionId
|| exec?.agent?.sessionId
|| '',
).trim()
}
async function requireOwnedJob(service, id, peer) {
function empNoFromUserWorkspacePath(cwd) {
const norm = String(cwd || '').replace(/\\/g, '/')
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
return match ? decodeURIComponent(match[1]) : null
}
/**
* Resolve web caller identity for ownership. IM peers keep conversation scoping.
*/
export function resolveToolIdentity(exec, service) {
const uds = typeof service?.getUdsAuth === 'function' ? service.getUdsAuth() : null
const sessionId = callerSessionId(exec)
let empNo = null
if (sessionId && uds?.getSessionOwner) {
empNo = uds.getSessionOwner(sessionId)
}
if (!empNo) {
empNo = empNoFromUserWorkspacePath(callerWorkingDirectory(exec))
}
if (!empNo) return null
return {
empNo: String(empNo),
displayName: String(empNo),
role: 'user',
permissions: { canViewAllSessions: false },
}
}
function forbidForeignJob(job, peer, identity = null) {
if (peer?.botId) {
if (jobVisibleToPeer(job, peer)) return job
const error = new Error('job not found or not owned by this chat')
error.code = 'NOT_FOUND'
throw error
}
if (identity?.empNo) {
assertCanAccessJob(job, identity)
return job
}
// No peer and no web identity: do not expose foreign jobs.
if (job?.ownerEmpNo && job.ownerEmpNo !== UNASSIGNED_OWNER) {
const error = new Error('job not found or not owned by this session')
error.code = 'NOT_FOUND'
throw error
}
return job
}
async function requireOwnedJob(service, id, peer, identity = null) {
const job = await service.getJob?.(id)
if (job) return forbidForeignJob(job, peer)
// Fallback when getJob is absent: list and find.
if (job) return forbidForeignJob(job, peer, identity)
const jobs = await service.listJobs()
const hit = (jobs || []).find((row) => row.id === id)
if (!hit) {
@ -146,7 +196,7 @@ async function requireOwnedJob(service, id, peer) {
error.code = 'NOT_FOUND'
throw error
}
return forbidForeignJob(hit, peer)
return forbidForeignJob(hit, peer, identity)
}
export function callerModelSelection(exec) {
@ -242,6 +292,7 @@ export function cronToolDefinitions(service, deps = {}) {
const dshIm = getDshIm()
try {
const peer = await resolveCallerPeer(exec, dshIm)
const identity = resolveToolIdentity(exec, service)
const delivery = await resolveCreateDelivery(args, exec, { dshIm })
const origin = await resolveCreateOrigin(args, exec, { dshIm })
const agentPreset = await resolveCreateAgentPreset(args, exec, {
@ -259,7 +310,7 @@ export function cronToolDefinitions(service, deps = {}) {
delivery,
...origin ? { origin } : {},
agentPreset,
})
}, identity)
return { job }
} catch (error) {
const tz = args.timezone || args.time_zone || 'Asia/Shanghai'
@ -302,7 +353,8 @@ export function cronToolDefinitions(service, deps = {}) {
async execute(args, exec) {
aborted(exec)
const peer = await resolveCallerPeer(exec, getDshIm())
let jobs = await service.listJobs()
const identity = resolveToolIdentity(exec, service)
let jobs = await service.listJobs(identity)
if (peer?.botId) jobs = jobs.filter((job) => jobVisibleToPeer(job, peer))
if (args?.enabled_only === true) jobs = jobs.filter((job) => job.enabled !== false)
return { jobs, count: jobs.length }
@ -325,9 +377,10 @@ export function cronToolDefinitions(service, deps = {}) {
async execute(args, exec) {
aborted(exec)
const peer = await resolveCallerPeer(exec, getDshIm())
const identity = resolveToolIdentity(exec, service)
const id = requireId(args)
await requireOwnedJob(service, id, peer)
const job = await service.pauseJob(id, false)
await requireOwnedJob(service, id, peer, identity)
const job = await service.pauseJob(id, false, identity)
return { job }
},
},
@ -348,9 +401,10 @@ export function cronToolDefinitions(service, deps = {}) {
async execute(args, exec) {
aborted(exec)
const peer = await resolveCallerPeer(exec, getDshIm())
const identity = resolveToolIdentity(exec, service)
const id = requireId(args)
await requireOwnedJob(service, id, peer)
const job = await service.pauseJob(id, true)
await requireOwnedJob(service, id, peer, identity)
const job = await service.pauseJob(id, true, identity)
return { job }
},
},
@ -379,9 +433,10 @@ export function cronToolDefinitions(service, deps = {}) {
async execute(args, exec) {
aborted(exec)
const peer = await resolveCallerPeer(exec, getDshIm())
const identity = resolveToolIdentity(exec, service)
const id = requireId(args)
try {
await requireOwnedJob(service, id, peer)
await requireOwnedJob(service, id, peer, identity)
await service.deleteJob(id)
return { id, deleted: true }
} catch (error) {