Keep shared external cron cwd (e.g. D:\code\gpt) instead of silent clamp.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-10 11:23:41 +08:00
parent 7a2a673ac2
commit 43facd608a
5 changed files with 111 additions and 14 deletions

View file

@ -6,7 +6,7 @@
import { randomUUID } from 'node:crypto'
import { mkdir } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, join } from 'node:path'
import { basename, dirname, join } from 'node:path'
import { claimOccurrence, executeClaimedRun, extractAssistantText, interruptActiveRuns, publicJob, settleRun, TITLE_PREFIX } from './fire.js'
import { assertDeliveryAllowedForIdentity, deliverRunToIm, mergeDeliveryMention, mirrorRunToSession, normalizeDelivery, normalizeOrigin } from './delivery.js'
import { apiError, resolveLocale } from './i18n.js'
@ -190,6 +190,9 @@ function allowsForeignJobCwd(identity, getUdsAuth, ownerEmpNo) {
const live = uds.resolveIdentityForEmpNo(empNo)
if (live && canViewAllJobs(live)) return true
}
if (typeof uds.getRole === 'function' && isElevatedCronRole(uds.getRole(empNo))) return true
// Match dsh-acl: local fallback cookie user is always elevated.
if (empNo === 'administrator') return true
return false
}
@ -205,6 +208,7 @@ function enrichIdentity(identity, getUdsAuth) {
|| identity.permissions?.canViewAllSessions
|| isElevatedCronRole(live.role)
|| isElevatedCronRole(identity.role)
|| identity.empNo === 'administrator'
)
return {
...identity,
@ -224,7 +228,7 @@ function enrichIdentity(identity, getUdsAuth) {
}
}
}
if (isElevatedCronRole(identity.role)) {
if (isElevatedCronRole(identity.role) || identity.empNo === 'administrator') {
return {
...identity,
permissions: {
@ -237,7 +241,25 @@ function enrichIdentity(identity, getUdsAuth) {
return identity
}
function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
export function normalizeFsPath(value) {
return String(value || '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
}
export function isPathInside(candidate, root) {
const cand = normalizeFsPath(candidate).toLowerCase()
const base = normalizeFsPath(root).toLowerCase()
if (!cand || !base) return false
return cand === base || cand.startsWith(`${base}/`)
}
/**
* Decide the durable cwd for a job.
* - Elevated owners: any explicit cwd
* - Owner tree (isUserPath): keep
* - Inside provision forest but not owner tree: clamp to ownerPath
* - Outside forest (shared trees like D:\\code\\gpt): keep when allowExternalCwd
*/
function resolveSanitizedCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo, allowExternalCwd = true } = {}) {
const raw = typeof cwd === 'string' ? cwd.trim() : ''
const uds = typeof getUdsAuth === 'function' ? getUdsAuth() : null
const owner = forOwnerEmpNo || identity?.empNo
@ -246,11 +268,36 @@ function sanitizeJobCwd(cwd, identity, getUdsAuth, { forOwnerEmpNo } = {}) {
: null
if (!raw) return ownerPath || ''
if (allowsForeignJobCwd(identity, getUdsAuth, owner)) return raw
// Without uds-auth, keep the caller cwd (tools may stamp owner from path only).
if (!uds) return raw
if (owner && uds?.isUserPath?.(owner, raw)) return raw
if (ownerPath) return ownerPath
return ''
if (ownerPath) {
const forest = dirname(ownerPath)
if (forest && isPathInside(raw, forest)) return ownerPath
if (allowExternalCwd !== false) return raw
return ownerPath
}
return allowExternalCwd !== false ? raw : ''
}
function sanitizeJobCwd(cwd, identity, getUdsAuth, opts = {}) {
const raw = typeof cwd === 'string' ? cwd.trim() : ''
const effective = resolveSanitizedCwd(cwd, identity, getUdsAuth, opts)
if (
raw
&& normalizeFsPath(raw).toLowerCase() !== normalizeFsPath(effective).toLowerCase()
&& opts.rejectClamp
) {
const error = new Error(
`cwd "${raw}" is not allowed for this user (effective "${effective || '(empty)'}"). `
+ 'Use a path under your provisioned workspace, a shared tree outside workspaceRoot '
+ '(when allowExternalCwd is on), or a super_admin / fallback_admin account.',
)
error.code = 'CWD_FORBIDDEN'
error.requestedCwd = raw
error.effectiveCwd = effective
throw error
}
return effective
}
function isTrustedApiRequest(request) {
@ -424,7 +471,14 @@ export function createHostService(options = {}) {
safeInput.delivery = delivery
safeInput.ownerEmpNo = ownerIdentity.empNo
safeInput.ownerDisplayName = ownerIdentity.displayName || ownerIdentity.empNo
safeInput.cwd = sanitizeJobCwd(safeInput.cwd, ownerIdentity, getUdsAuth, { forOwnerEmpNo: ownerIdentity.empNo })
const snap = await snapshot()
const allowExternalCwd = snap?.settings?.allowExternalCwd !== false
const requestedCwd = String(safeInput.cwd || '').trim()
safeInput.cwd = sanitizeJobCwd(safeInput.cwd, ownerIdentity, getUdsAuth, {
forOwnerEmpNo: ownerIdentity.empNo,
allowExternalCwd,
rejectClamp: !!requestedCwd,
})
} else if (safeInput.ownerEmpNo) {
safeInput.ownerEmpNo = normalizeOwnerEmpNo(safeInput.ownerEmpNo)
} else {
@ -482,11 +536,17 @@ export function createHostService(options = {}) {
nextInput.delivery = mergeDeliveryMention(job.delivery, patch.delivery)
assertDeliveryAllowedForIdentity(nextInput.delivery, enriched)
}
const allowExternalCwd = current.settings?.allowExternalCwd !== false
const requestedCwd = patch.cwd !== undefined ? String(patch.cwd || '').trim() : ''
nextInput.cwd = sanitizeJobCwd(
nextInput.cwd,
enriched,
getUdsAuth,
{ forOwnerEmpNo: nextInput.ownerEmpNo },
{
forOwnerEmpNo: nextInput.ownerEmpNo,
allowExternalCwd,
rejectClamp: !!requestedCwd,
},
)
}
if (patch.ownerEmpNo !== undefined && canViewAllJobs(enrichIdentity(patch._identity, getUdsAuth))) {
@ -1089,7 +1149,8 @@ export function ownerAllowsForeignCwd(ownerEmpNo, uds) {
/**
* Placement order:
* 1. Explicit job.cwd (kept for super_admin owners; clamped to owner tree for users)
* 1. Explicit job.cwd (kept for elevated owners; shared trees outside forest kept when allowExternalCwd;
* clamped to owner tree only when inside provision forest but not the owner's path)
* 2. Owner provisioned path (multi-user)
* 3. Recent registry workspace — only standalone / non-IM unassigned
* 4. Shared ops-cron fallback
@ -1105,12 +1166,19 @@ export function resolveSessionPlacement(ctx, job = {}, deps = {}) {
? uds.getProvisionedWorkspacePath(ownerEmpNo)
: null
const imOrigin = normalizeOrigin(job?.origin)?.kind === 'im'
const allowExternalCwd = deps.allowExternalCwd !== false
let requested = String(job?.cwd || '').trim()
if (requested && ownerEmpNo && !ownerEmpNo.startsWith('__') && uds?.isUserPath) {
const allowForeign = ownerAllowsForeignCwd(ownerEmpNo, uds)
if (!uds.isUserPath(ownerEmpNo, requested) && !allowForeign && ownerPath) {
requested = ownerPath
const forest = dirname(ownerPath)
if (forest && isPathInside(requested, forest)) {
requested = ownerPath
} else if (allowExternalCwd === false) {
requested = ownerPath
}
// else: outside forest → keep requested (shared business cwd)
}
}
if (requested) return { cwd: requested, workspace: match(requested), missingCwd: false }
@ -1390,7 +1458,7 @@ export function makeLiveSessionPort(ctx) {
}
const sessionId = randomUUID()
const udsAuth = tryGet(ctx, 'udsAuth')
const placement = resolveSessionPlacement(ctx, job, { udsAuth })
const placement = resolveSessionPlacement(ctx, job, { udsAuth, allowExternalCwd: true })
if (placement.missingCwd) {
const error = new Error('IM scheduled job has no cwd; recreate it from the channel chat')
error.code = 'IM_JOB_MISSING_CWD'