Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions services/runner/src/engines/sandbox_agent/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
type FetchedAttachment,
} from "../../sessions/attachments.ts";
import { attachmentDeliveryUnsupportedMessage } from "./capabilities.ts";
import type { RunPlan } from "./run-plan.ts";
import type { RunPlan, RunPlanWorkspace } from "./run-plan.ts";
import { COLD_FRAME_USER_LABEL } from "./transcript.ts";

export type AttachmentDeliveryOutcome =
Expand Down Expand Up @@ -83,8 +83,10 @@ export interface AttachmentSandbox {
}) => Promise<{ exitCode?: number } | undefined>;
}

type MaterializePlan = Pick<RunPlan, "cwd" | "isDaytona">;
type DeliveryPlan = Pick<RunPlan, "cwd" | "isDaytona" | "acpAgent" | "harness">;
type MaterializePlan = Pick<RunPlan, "isDaytona"> & {
workspace: Pick<RunPlanWorkspace, "cwd">;
};
type DeliveryPlan = MaterializePlan & Pick<RunPlan, "acpAgent" | "harness">;
type Auth = () => string;
type Log = (message: string) => void;

Expand Down Expand Up @@ -398,7 +400,7 @@ export async function materializeWorkingCopy(
ref: AttachmentRef,
bytes: Uint8Array,
): Promise<"written" | "exists"> {
const path = attachmentWorkingPath(plan.cwd, ref);
const path = attachmentWorkingPath(plan.workspace.cwd, ref);
return plan.isDaytona
? daytonaMaterialize(sandbox, path, bytes)
: localMaterialize(path, bytes);
Expand Down Expand Up @@ -637,7 +639,7 @@ async function workingCopyExists(
plan: MaterializePlan,
ref: AttachmentRef,
): Promise<boolean> {
const path = attachmentWorkingPath(plan.cwd, ref);
const path = attachmentWorkingPath(plan.workspace.cwd, ref);
if (plan.isDaytona) {
if (typeof sandbox.statFs !== "function") return false;
await rejectDaytonaSymlinks(sandbox, [
Expand Down Expand Up @@ -839,7 +841,7 @@ export async function resolveCurrentTurnAttachments(input: {
const authoritative = verifiedRef(ref, fetched);
let path: AttachmentPath;
try {
path = attachmentWorkingPath(input.plan.cwd, authoritative);
path = attachmentWorkingPath(input.plan.workspace.cwd, authoritative);
await materializeWorkingCopy(
input.sandbox,
input.plan,
Expand Down
43 changes: 30 additions & 13 deletions services/runner/src/engines/sandbox_agent/codex-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { existsSync, mkdirSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";

import type { RunPlan } from "./run-plan.ts";
import type {
RunPlan,
RunPlanCredentials,
RunPlanWorkspace,
} from "./run-plan.ts";

type Log = (message: string) => void;

Expand All @@ -44,6 +48,17 @@ export function codexHomeDir(cwd: string): string {
* constant across a session's turns and is NOT a config-fingerprint input, preserving warm daemon
* reuse.
*/
/** The slice that decides which Codex auth mode a run is in. */
type CodexModePlan = Pick<RunPlan, "acpAgent"> & {
credentials: Pick<RunPlanCredentials, "credentialMode">;
};

/** The mode slice plus where the run's Codex home is rooted. */
type CodexHomePlan = CodexModePlan &
Pick<RunPlan, "isDaytona"> & {
workspace: Pick<RunPlanWorkspace, "cwd">;
};

export function codexSqliteHomeDir(cwd: string): string {
return join(tmpdir(), "agenta", "codex-sqlite", basename(cwd));
}
Expand All @@ -54,18 +69,20 @@ export function codexSqliteHomeDir(cwd: string): string {
* own mounted OAuth login instead, so it is excluded here.
*/
export function isManagedCodexRun(
plan: Pick<RunPlan, "acpAgent" | "credentialMode">,
plan: CodexModePlan,
): boolean {
return (
plan.acpAgent === "codex" && plan.credentialMode !== "runtime_provided"
plan.acpAgent === "codex" &&
plan.credentials.credentialMode !== "runtime_provided"
);
}

export function isSubscriptionCodexRun(
plan: Pick<RunPlan, "acpAgent" | "credentialMode">,
plan: CodexModePlan,
): boolean {
return (
plan.acpAgent === "codex" && plan.credentialMode === "runtime_provided"
plan.acpAgent === "codex" &&
plan.credentials.credentialMode === "runtime_provided"
);
}

Expand All @@ -86,18 +103,18 @@ function codexSubscriptionMountDir(): string | undefined {
* for best-effort teardown cleanup. Subscription additionally pins the credential store to `file`.
*/
export function configureCodexHome(
plan: Pick<RunPlan, "acpAgent" | "credentialMode" | "isDaytona" | "cwd">,
plan: CodexHomePlan,
env: Record<string, string>,
): string | undefined {
// Local codex only (managed or subscription). Daytona and non-codex runs are no-ops.
if (plan.acpAgent !== "codex" || plan.isDaytona) return undefined;
// Runner-owned per-session home in both modes. For subscription this overrides the operator's
// mount path that buildDaemonEnv inherited into env.CODEX_HOME, so only the auth.json we symlink
// in (see symlinkCodexSubscriptionAuthFile) is visible — not the operator's config/plugins/apps.
env.CODEX_HOME = codexHomeDir(plan.cwd);
env.CODEX_HOME = codexHomeDir(plan.workspace.cwd);
// Both modes redirect SQLite off the home so neither the geesefs cwd nor the operator mount
// accumulates per-run WAL SQLite.
const sqliteHome = codexSqliteHomeDir(plan.cwd);
const sqliteHome = codexSqliteHomeDir(plan.workspace.cwd);
mkdirSync(sqliteHome, { recursive: true });
env.CODEX_SQLITE_HOME = sqliteHome;
// Subscription: pin the credential store to `file` so a keyring/auto mode (from any config layer)
Expand Down Expand Up @@ -133,12 +150,12 @@ export function codexDaytonaSqliteHomeDir(cwd: string): string {
* (run-plan.ts); local runs and non-codex runs are no-ops.
*/
export function configureDaytonaCodexEnv(
plan: Pick<RunPlan, "acpAgent" | "credentialMode" | "isDaytona" | "cwd">,
plan: CodexHomePlan,
daytonaEnv: Record<string, string>,
): void {
if (!plan.isDaytona || !isManagedCodexRun(plan)) return;
daytonaEnv.CODEX_HOME = codexHomeDir(plan.cwd);
daytonaEnv.CODEX_SQLITE_HOME = codexDaytonaSqliteHomeDir(plan.cwd);
daytonaEnv.CODEX_HOME = codexHomeDir(plan.workspace.cwd);
daytonaEnv.CODEX_SQLITE_HOME = codexDaytonaSqliteHomeDir(plan.workspace.cwd);
}

/**
Expand All @@ -151,7 +168,7 @@ export function configureDaytonaCodexEnv(
* file-free and never reach here.
*/
export function symlinkCodexSubscriptionAuthFile(
plan: Pick<RunPlan, "acpAgent" | "credentialMode" | "isDaytona" | "cwd">,
plan: CodexHomePlan,
log: Log = () => {},
): void {
if (!isSubscriptionCodexRun(plan) || plan.isDaytona) return;
Expand All @@ -162,7 +179,7 @@ export function symlinkCodexSubscriptionAuthFile(
return;
}

const home = codexHomeDir(plan.cwd);
const home = codexHomeDir(plan.workspace.cwd);
mkdirSync(home, { recursive: true, mode: 0o700 });
const linkPath = join(home, "auth.json");
if (existsSync(linkPath)) return;
Expand Down
36 changes: 22 additions & 14 deletions services/runner/src/engines/sandbox_agent/daytona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
serializePiModelsJson,
type PiModelConfigPlan,
} from "./pi-model-config.ts";
import { type RunPlan } from "./run-plan.ts";
import {
type RunPlan,
type RunPlanPrompt,
type RunPlanWorkspace,
} from "./run-plan.ts";

type Log = (message: string) => void;

Expand Down Expand Up @@ -190,14 +194,13 @@ export async function removePiModelsConfigFromSandbox(

export interface PrepareDaytonaPiAssetsInput {
sandbox: any;
plan: Pick<
RunPlan,
| "isPi"
| "skillDirs"
| "hasSystemPrompt"
| "systemPrompt"
| "appendSystemPrompt"
>;
plan: Pick<RunPlan, "isPi"> & {
workspace: Pick<RunPlanWorkspace, "skillDirs">;
prompt: Pick<
RunPlanPrompt,
"hasSystemPrompt" | "systemPrompt" | "appendSystemPrompt"
>;
};
/**
* A managed OpenAI-compatible custom run's Pi provider config. When set, its `models.json` is
* uploaded before the ACP session starts; when absent, any stale `models.json` on a reused
Expand Down Expand Up @@ -243,15 +246,20 @@ export async function prepareDaytonaPiAssets({
} else {
await removePiModelsConfigFromSandbox(sandbox, DAYTONA_PI_DIR, log);
}
if (plan.skillDirs.length > 0) {
await uploadSkillsToSandbox(sandbox, DAYTONA_PI_DIR, plan.skillDirs, log);
if (plan.workspace.skillDirs.length > 0) {
await uploadSkillsToSandbox(
sandbox,
DAYTONA_PI_DIR,
plan.workspace.skillDirs,
log,
);
}
if (plan.hasSystemPrompt) {
if (plan.prompt.hasSystemPrompt) {
await uploadSystemPromptToSandbox(
sandbox,
DAYTONA_PI_DIR,
plan.systemPrompt,
plan.appendSystemPrompt,
plan.prompt.systemPrompt,
plan.prompt.appendSystemPrompt,
log,
);
}
Expand Down
47 changes: 29 additions & 18 deletions services/runner/src/engines/sandbox_agent/environment-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,22 +161,27 @@ export async function prepareEnvironmentSetup(
if (!planResult.ok) return { ok: false as const, error: planResult.error };
const plan = planResult.plan;
const piSkillSnapshot = resolvePiSkillSnapshot(plan);
const agentMountDir = agentMountCreds ? agentMountPath(plan.cwd) : undefined;
const agentMountDir = agentMountCreds
? agentMountPath(plan.workspace.cwd)
: undefined;

// Clear-then-apply (Security rule 5): on a managed run (credentialMode "env") the daemon
// inherits NONE of the sidecar's own provider keys, so only the resolved
// `plan.modelEnvironment` is present and an inherited key for another provider cannot leak.
// `plan.credentials.modelEnvironment` is present and an inherited key for another
// provider cannot leak.
// "none" asserts NO credential (connections/models.py), so it clears too — otherwise the
// daemon would inherit the declared provider's keys (e.g. OPENAI_API_KEY) from the sidecar.
// Only runtime_provided keeps the inherited keys: the harness uses its own login there.
const clearProviderEnv =
plan.credentialMode === "env" || plan.credentialMode === "none";
plan.credentials.credentialMode === "env" ||
plan.credentials.credentialMode === "none";
const env = (deps.buildDaemonEnv ?? buildDaemonEnv)(plan.acpAgent, {
clearProviderEnv,
provider: request.modelConnection?.provider,
deployment: request.modelConnection?.deployment,
});
Object.assign(env, plan.modelEnvironment); // apply only the resolved provider keys
// apply only the resolved provider keys
Object.assign(env, plan.credentials.modelEnvironment);
applyClaudeConnectionEnv(env, request, plan.acpAgent, logger);
const piSessionDir = configurePiSessionWorkspace(plan, env);
configurePiSkillSnapshot(piSkillSnapshot, env);
Expand All @@ -187,22 +192,24 @@ export async function prepareEnvironmentSetup(
// local Pi's OTLP bearer rides a runner-written 0600 file, never a plain env var —
// Daytona never receives telemetry env here at all (`!plan.isDaytona` gates it off above).
const otlpAuthFilePath =
plan.isPi && !plan.isDaytona ? `${plan.relayDir}.otlp-auth` : undefined;
plan.isPi && !plan.isDaytona
? `${plan.workspace.relayDir}.otlp-auth`
: undefined;
const otlpAuthorization =
request.telemetry?.exporters?.otlp?.headers?.authorization;
if (otlpAuthFilePath && otlpAuthorization) {
writeOtlpAuthFile(otlpAuthFilePath, otlpAuthorization, logger);
}
const piExtEnv = plan.isPi
? buildPiExtensionEnv(request, !plan.isDaytona, {
relayDir: plan.relayDir,
usageOutPath: plan.usageOutPath,
relayDir: plan.workspace.relayDir,
usageOutPath: plan.workspace.usageOutPath,
otlpAuthFilePath,
builtinGatingActive: plan.builtinGatingActive,
builtinGatingActive: plan.tools.builtinGatingActive,
// The materialized skill names (author + forced `_agenta.*`) so Pi's own agent span
// records which skills loaded; local Pi self-instruments, so the runner's sandbox-agent
// otel has no span to stamp here.
skills: plan.skillDirs.map((s) => s.name),
skills: plan.workspace.skillDirs.map((s) => s.name),
})
: {};
// Daytona's provider is built from `piExtEnv` rather than the local daemon env. Keep the
Expand All @@ -218,11 +225,11 @@ export async function prepareEnvironmentSetup(
configureDaytonaCodexEnv(plan, piExtEnv);
Object.assign(env, piExtEnv); // local daemon inherits it; daytona gets it via envVars
logger(
`tools=${plan.toolSpecs.length} executableTools=${plan.executableToolSpecs.length} ` +
`tools=${plan.tools.toolSpecs.length} executableTools=${plan.tools.executableToolSpecs.length} ` +
`piPublicTools=${piExtEnv.AGENTA_AGENT_TOOLS_PUBLIC_SPECS ? "yes" : "no"}`,
);
if (!plan.isPi && plan.isDaytona) {
const clientTools = plan.toolSpecs
const clientTools = plan.tools.toolSpecs
.filter((spec) => spec.kind === "client")
.map((spec) => spec.name);
if (clientTools.length > 0) {
Expand All @@ -243,12 +250,14 @@ export async function prepareEnvironmentSetup(
if (plan.isPi) {
try {
// The presence check consults the FULL materialized model environment: on a Daytona
// Secrets run the opaque key left `plan.modelEnvironment` for the secret plan, but the
// sandbox still receives its binding (as a Daytona Secret attachment).
// Secrets run the opaque key left `plan.credentials.modelEnvironment` for the
// secret plan, but the sandbox still receives its binding (as a Daytona Secret
// attachment).
const fullModelEnvironment: Record<string, string> = {
...plan.modelEnvironment,
...plan.credentials.modelEnvironment,
};
for (const candidate of plan.daytonaSecretPlan?.candidates ?? []) {
const secretCandidates = plan.credentials.daytonaSecretPlan?.candidates;
for (const candidate of secretCandidates ?? []) {
if (candidate.consumer.kind === "model") {
fullModelEnvironment[candidate.binding.name] = candidate.value;
}
Expand Down Expand Up @@ -295,7 +304,7 @@ export async function prepareEnvironmentSetup(
const localBuiltinGatingUnenforceable =
plan.isPi &&
!plan.isDaytona &&
plan.builtinGatingActive &&
plan.tools.builtinGatingActive &&
!localPiAssets.extensionInstalled;
// Fail closed: a Pi run whose provider routing rides the extension's model endpoint override
// (`model-provider-override.ts`, set in `buildPiExtensionEnv`) cannot run without the
Expand All @@ -315,7 +324,9 @@ export async function prepareEnvironmentSetup(
// lifecycle, exactly like a normal local install (interface.md section 6). buildRunPlan already
// rejected a runtime_provided Claude run with no configured CLAUDE_CONFIG_DIR.

logger(`harness=${plan.harness} sandbox=${plan.sandboxId} cwd=${plan.cwd}`);
logger(
`harness=${plan.harness} sandbox=${plan.sandboxId} cwd=${plan.workspace.cwd}`,
);

// The resolved model ref as it reaches the runner (key NAMES only, never values) — the one
// line that answers "what model/provider/deployment/credential did this run actually use".
Expand Down Expand Up @@ -391,7 +402,7 @@ export async function prepareEnvironmentSetup(
? undefined
: {
cleanup: async () =>
rmSync(plan.cwd, { recursive: true, force: true }),
rmSync(plan.workspace.cwd, { recursive: true, force: true }),
},
runtimeRemount: undefined,
closeToolMcp: undefined,
Expand Down
Loading
Loading