mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 03:03:18 +08:00
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:
parent
7a2a673ac2
commit
43facd608a
5 changed files with 111 additions and 14 deletions
90
lib/host.js
90
lib/host.js
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue