diff --git a/src/mgr/kubernetes-runner-job.ts b/src/mgr/kubernetes-runner-job.ts index 65fb566..fd5c7ff 100644 --- a/src/mgr/kubernetes-runner-job.ts +++ b/src/mgr/kubernetes-runner-job.ts @@ -142,7 +142,9 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore; const pvcName = refreshed?.storagePvcName ?? ensured.pvcName; if (!pvcName) throw new AgentRunError("infra-failed", `session ${run.sessionRef.sessionId} PVC was not resolved for runner job`, { httpStatus: 502 }); const subdir = refreshed?.codexRolloutSubdir ?? ensured.codexRolloutSubdir ?? "sessions"; - sessionPvc = { pvcName, namespace: refreshed?.storageNamespace ?? ensured.namespace ?? namespace, mountPath: `/home/agentrun/.codex-${run.backendProfile}/${subdir}`, codexRolloutSubdir: subdir }; + const mountPath = `/home/agentrun/.codex-${run.backendProfile}/${subdir}`; + const workspacePath = `${mountPath}/agentrun-workspace`; + sessionPvc = { pvcName, namespace: refreshed?.storageNamespace ?? ensured.namespace ?? namespace, mountPath, codexRolloutSubdir: subdir, workspacePath }; sessionPvcSummary = { sessionId: run.sessionRef.sessionId, pvcName: sessionPvc.pvcName, @@ -150,6 +152,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore; pvcPhase: refreshed?.storagePvcPhase ?? ensured.pvcPhase ?? null, mountPath: sessionPvc.mountPath, codexRolloutSubdir: sessionPvc.codexRolloutSubdir, + workspacePath: sessionPvc.workspacePath ?? null, valuesPrinted: false, }; if (ensured.pvcPhase === "NotFound" || ensured.pvcPhase === "Unknown") { diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index 7ba5f78..47f0487 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -68,6 +68,7 @@ export interface RunnerSessionPvcOptions { namespace: string; mountPath: string; codexRolloutSubdir: string; + workspacePath?: string; } export interface RunnerTransientEnv { @@ -279,6 +280,8 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string { name: "AGENTRUN_SESSION_PVC_NAMESPACE", value: context.sessionPvc.namespace }, { name: "AGENTRUN_SESSION_PVC_MOUNT_PATH", value: context.sessionPvc.mountPath }, { name: "AGENTRUN_CODEX_ROLLOUT_SUBDIR", value: context.sessionPvc.codexRolloutSubdir }, + { name: "AGENTRUN_WORKSPACE_PATH", value: context.sessionPvc.workspacePath ?? `${context.sessionPvc.mountPath}/agentrun-workspace` }, + { name: "AGENTRUN_SESSION_WORKSPACE_PATH", value: context.sessionPvc.workspacePath ?? `${context.sessionPvc.mountPath}/agentrun-workspace` }, ] : []), ...runnerEgressProxyEnvVars(), ...runnerGitTransportEnvVars(), diff --git a/src/runner/resource-bundle.ts b/src/runner/resource-bundle.ts index d7f0f82..1aa10b6 100644 --- a/src/runner/resource-bundle.ts +++ b/src/runner/resource-bundle.ts @@ -88,7 +88,11 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl const runScope = env.AGENTRUN_RUN_ID ?? env.AGENTRUN_ATTEMPT_ID ?? "standalone"; const assemblyRoot = path.join(workspaceRoot, `gitbundle-${stableHash({ runScope, resourceBundleRef }).slice(0, 16)}`); const checkoutRoot = path.join(assemblyRoot, "checkouts"); - const workspacePath = path.join(assemblyRoot, "workspace"); + const explicitWorkspacePath = optionalNonEmpty(env.AGENTRUN_WORKSPACE_PATH); + const workspacePath = explicitWorkspacePath ? path.resolve(explicitWorkspacePath) : path.join(assemblyRoot, "workspace"); + if (explicitWorkspacePath && isSameOrChildPath(workspacePath, assemblyRoot)) { + throw new AgentRunError("schema-invalid", "AGENTRUN_WORKSPACE_PATH must not be inside transient resource assembly root", { httpStatus: 400, details: { workspacePath: pathSummary(workspacePath), assemblyRoot: pathSummary(assemblyRoot), valuesPrinted: false } }); + } await rm(assemblyRoot, { recursive: true, force: true }); await mkdir(checkoutRoot, { recursive: true }); await mkdir(workspacePath, { recursive: true }); @@ -130,6 +134,7 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl treeId: defaultCheckout.treeId, checkoutPath: pathSummary(defaultCheckout.checkoutPath), workspacePath: pathSummary(workspacePath), + workspacePersistence: explicitWorkspacePath ? { mode: "session", path: pathSummary(workspacePath), valuesPrinted: false } : { mode: "run", path: pathSummary(workspacePath), valuesPrinted: false }, gitTransport: gitTransportSummary(), bundles: { count: materializedBundles.length, @@ -146,6 +151,11 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl }; } +function isSameOrChildPath(candidate: string, parent: string): boolean { + const relative = path.relative(parent, candidate); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + function gitMirrorConfig(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): GitMirrorConfig | undefined { void resourceBundleRef; return defaultGitMirrorConfig(env);