@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
1,673 lines • 80.8 kB
JavaScript
import { refreshPreparedAgentTools } from "../../agentToolConfiguration.mjs";
import logger, { logModelAndToolActionWarning } from "../../logger.mjs";
import { UserError } from "../../errors.mjs";
import { isMemoryCapability } from "../capabilities/memory.mjs";
import { SandboxLifecycleError, SandboxMountError } from "../errors.mjs";
import { cloneManifest, isEnvValueReference, manifestAcknowledgesInContainerMountCredentialExposure, Manifest, replaceManifestMountCredentialExposurePolicy, } from "../manifest.mjs";
import { captureLiveMountCredentialAuthorityIfAbsent, assertSandboxSessionStateUsable, isSandboxSessionStateUnsafe, liveMountCredentialAuthorityMatches, liveMountEnvironmentAuthorityMatches, manifestHasInContainerMounts, manifestHasNonResumableMountAuthority, recordLiveMountCredentialAuthority, resolveAndValidateMountEnvironment, validateMountCredentialBoundaries, validateMountEnvironmentCredentialBoundaries, withExclusiveSandboxStateInspection, } from "../mountSecurity.mjs";
import { isDefaultRemoteMountCommandAllowlist } from "../shared/remoteMountCommandAllowlist.mjs";
import { serializeManifestEnvironment } from "../shared/environment.mjs";
import { stableJsonStringify } from "../shared/stableJson.mjs";
import { getOrCreateSandboxMemoryGenerationManager, } from "../memory/generation.mjs";
import { cloneSandboxCapabilities, prepareSandboxAgent, } from "./agentPreparation.mjs";
import { acquireSandboxAgent, allocateAgentKeys, getObjectId, indexAgentsByKey, isSandboxAgent, releaseSandboxAgents, } from "./agentKeys.mjs";
import { forgetLivePreservedOwnedSessions, forgetLivePreservedOwnedSessionHandle, hasRejectedLivePreservedOwnedSessions, livePreservedOwnedSessionEntries, livePreservedOwnedSession, preservedOwnedSessionAgentKeysWithoutLiveReuse, rejectLivePreservedOwnedSessionHandle, rememberRejectedLivePreservedOwnedSessionHandle, rememberLivePreservedOwnedSessions, } from "./livePreservedSessions.mjs";
import { applyManifestToProvidedSession, withExclusiveProvidedSessionManifestMutation, } from "./providedSessionManifest.mjs";
import { serializeSandboxRuntimeState } from "./sessionSerialization.mjs";
import { assertMountCredentialsRebound, assertHostPathGrantsRebound, rebindPersistedMountCredentials, rebindPersistedPathGrants, } from "../sandboxes/shared/manifestPersistence.mjs";
import { cleanupSandboxSession, hasSessionCleanup, runSandboxSessionPreStop, runSandboxSessionPreStopHooks, } from "./sessionLifecycle.mjs";
import { assertSessionStateEnvelope, assertTrustedManifestForDockerRunState, clientPreparedManifestAuthoritiesMatch, deserializeSandboxSessionStateEntry, getPreviousSerializedSessionsByAgent, getSerializedSandboxState, getSerializedSessionEntryForAgent, hasPreservedOwnedSessions, mergeClientPreparedManifestAuthority, resolveTrustedManifestForResume, serializedSessionEntryRequiresFreshMountAuthority, } from "./sessionState.mjs";
import { withSandboxSpan } from "./spans.mjs";
import { manifestWithRunAsUser, sandboxRunAsName } from "./runAsManifest.mjs";
import { SandboxWorkspaceScope } from "../workspacePaths.mjs";
const sandboxAgentIdentitiesByRunState = new WeakMap();
export class SandboxRuntimeManager {
sandboxConfig;
workspaceScope;
runState;
agentKeys;
agentsByKey;
acquiredAgents = new Map();
sessionsByAgent = new Map();
sessionsByAgentKey = new Map();
sessionAgentNamesByKey = new Map();
preparedAgents = new Map();
preparedSessions = new Map();
preparedManifestSignatures = new Map();
ownedSessionAgentKeys = new Set();
sessionStartPromises = new WeakMap();
activeMemory;
currentAgentId;
constructor(args) {
this.sandboxConfig = args.sandboxConfig;
this.workspaceScope = new SandboxWorkspaceScope(args.sandboxConfig?.cwd);
this.runState = args.runState;
const existingIdentityRegistry = args.runState
? sandboxAgentIdentitiesByRunState.get(args.runState)
: undefined;
if (existingIdentityRegistry) {
this.agentKeys = existingIdentityRegistry.agentKeys;
this.agentsByKey = existingIdentityRegistry.agentsByKey;
}
else {
this.agentKeys = allocateAgentKeys(args.startingAgent);
this.agentsByKey = indexAgentsByKey(args.startingAgent, this.agentKeys);
if (args.runState) {
sandboxAgentIdentitiesByRunState.set(args.runState, {
agentKeys: this.agentKeys,
agentsByKey: this.agentsByKey,
});
}
}
}
async prepareAgent(args) {
const { currentAgent, turnInput, runConfigModel, tracingParent } = args;
if (!isSandboxAgent(currentAgent)) {
this.activeMemory = undefined;
return {
executionAgent: currentAgent,
turnInput,
};
}
if (!this.sandboxConfig) {
throw new UserError('SandboxAgent execution requires `RunConfig.sandbox`.');
}
return await withSandboxSpan('sandbox.prepare_agent', {
agent_name: currentAgent.name,
}, async (prepareSpan) => {
this.acquireAgent(currentAgent);
const session = await this.ensureSession(currentAgent, prepareSpan);
await this.ensureWorkspaceScopeAccessible(session, sandboxRunAsName(currentAgent.runAs));
// Bind a clone to the live session so capability tools and instructions can carry
// per-session state without mutating the public SandboxAgent instance.
const executionAgent = this.getPreparedAgent(currentAgent, session, runConfigModel, tracingParent);
const memory = executionAgent.capabilities.find(isMemoryCapability);
if (memory) {
this.activeMemory = {
session,
memory,
runAs: sandboxRunAsName(currentAgent.runAs),
};
}
else {
this.activeMemory = undefined;
}
return {
executionAgent,
turnInput: executionAgent.capabilities.reduce((input, capability) => capability.processContext(input), turnInput),
};
}, tracingParent);
}
async cleanup(state, options = {}) {
const preserveOwnedSessions = options.preserveOwnedSessions ?? false;
const runCleanup = async (cleanupSpan) => {
let preserveCleanupHandles = false;
try {
const cleanupPlan = await this.planCleanup(state, { preserveOwnedSessions }, cleanupSpan);
await this.executeCleanupPlan(state, cleanupPlan, {
onCloseError: () => {
preserveCleanupHandles = true;
},
tracingParent: cleanupSpan,
});
}
finally {
if (hasPreservedOwnedSessions(getSerializedSandboxState(state))) {
sandboxAgentIdentitiesByRunState.set(state, {
agentKeys: this.agentKeys,
agentsByKey: this.agentsByKey,
});
}
else {
sandboxAgentIdentitiesByRunState.delete(state);
}
this.releaseAgents();
this.sessionsByAgent.clear();
if (!preserveCleanupHandles) {
this.sessionsByAgentKey.clear();
this.sessionAgentNamesByKey.clear();
this.ownedSessionAgentKeys.clear();
}
this.preparedAgents.clear();
this.preparedSessions.clear();
this.preparedManifestSignatures.clear();
this.activeMemory = undefined;
this.currentAgentId = undefined;
}
};
if (this.sessionsByAgentKey.size === 0 &&
this.preparedAgents.size === 0 &&
this.ownedSessionAgentKeys.size === 0) {
await runCleanup(options.tracingParent);
return;
}
await withSandboxSpan('sandbox.cleanup', {}, runCleanup, options.tracingParent);
}
async planCleanup(state, options, tracingParent) {
if (this.sessionsByAgentKey.size > 0) {
return await this.planCleanupForActiveSessions(state, options);
}
return await this.planCleanupForSerializedStateOnly(state, options, tracingParent);
}
async planCleanupForActiveSessions(state, options) {
const cleanupPlan = options.preserveOwnedSessions
? {}
: {
ownedSessionCloseTarget: 'all',
afterOwnedSessionClose: {
forgetLivePreservedSessions: true,
},
};
const { closeErrors: rejectedCloseErrors } = await this.closeLivePreservedOwnedSessions(state, {
rejectedOnly: true,
});
if (rejectedCloseErrors.length > 0) {
cleanupPlan.deferredError = lifecycleErrorForCloseErrors('Failed to close one or more rejected live sandbox sessions.', rejectedCloseErrors);
}
try {
await this.runPreStopHooksBeforeRelease();
const serializedState = await this.serializeState({
includeOwnedSessions: options.preserveOwnedSessions,
});
state._sandbox = serializedState;
if (options.preserveOwnedSessions && serializedState) {
rememberLivePreservedOwnedSessions({
state,
serializedState,
sessionsByAgentKey: this.sessionsByAgentKey,
});
const serializedOnlySessionAgentKeys = preservedOwnedSessionAgentKeysWithoutLiveReuse(serializedState);
if (serializedOnlySessionAgentKeys.size > 0) {
cleanupPlan.ownedSessionCloseTarget = serializedOnlySessionAgentKeys;
cleanupPlan.afterOwnedSessionClose = {
removeFreshCreationSessionsFromSandboxState: true,
};
}
}
if (options.preserveOwnedSessions &&
!serializedState &&
this.ownedSessionAgentKeys.size > 0) {
cleanupPlan.ownedSessionCloseTarget = 'all';
cleanupPlan.afterOwnedSessionClose = {
forgetLivePreservedSessions: true,
};
}
}
catch (error) {
cleanupPlan.deferredError = cleanupPlan.deferredError
? new SandboxLifecycleError('Failed to clean up rejected live sessions and serialize active sandbox sessions.', { errors: [cleanupPlan.deferredError, error] })
: error;
if (this.ownedSessionAgentKeys.size > 0) {
cleanupPlan.ownedSessionCloseTarget = 'all';
cleanupPlan.afterOwnedSessionClose = {
forgetLivePreservedSessions: true,
removeClosedSessionsFromSandboxState: true,
};
}
}
return cleanupPlan;
}
async planCleanupForSerializedStateOnly(state, options, tracingParent) {
const cleanupPlan = options.preserveOwnedSessions
? {}
: {
ownedSessionCloseTarget: 'all',
afterOwnedSessionClose: {
forgetLivePreservedSessions: true,
},
};
let sandboxState = getSerializedSandboxState(state);
if ((hasPreservedOwnedSessions(sandboxState) ||
hasRejectedLivePreservedOwnedSessions(state)) &&
!options.preserveOwnedSessions) {
const { closedAgentKeys, closeErrors } = await this.closeLivePreservedOwnedSessions(state);
if (closedAgentKeys.size > 0) {
if (sandboxState &&
!removeClosedPreservedOwnedSessions(sandboxState, closedAgentKeys)) {
state._sandbox = undefined;
sandboxState = undefined;
}
}
if (closeErrors.length === 1) {
throw closeErrors[0];
}
if (closeErrors.length > 1) {
throw new SandboxLifecycleError('Failed to close one or more live preserved owned sandbox sessions.', { errors: closeErrors });
}
if (hasPreservedOwnedSessions(sandboxState)) {
if (this.sandboxConfig?.client) {
try {
await this.adoptPreservedOwnedSessions(tracingParent);
if (this.ownedSessionAgentKeys.size > 0) {
cleanupPlan.ownedSessionCloseTarget = 'all';
cleanupPlan.afterOwnedSessionClose = {
clearSandboxState: true,
forgetLivePreservedSessions: true,
};
}
else {
cleanupPlan.ownedSessionCloseTarget = undefined;
cleanupPlan.afterOwnedSessionClose = undefined;
}
}
catch (error) {
cleanupPlan.deferredError = error;
}
}
else {
state._sandbox = undefined;
forgetLivePreservedOwnedSessions(state);
cleanupPlan.ownedSessionCloseTarget = undefined;
cleanupPlan.afterOwnedSessionClose = undefined;
}
}
else if (closedAgentKeys.size > 0) {
state._sandbox = undefined;
}
}
else if (!hasPreservedOwnedSessions(sandboxState)) {
state._sandbox = undefined;
forgetLivePreservedOwnedSessions(state);
}
return cleanupPlan;
}
async executeCleanupPlan(state, plan, options) {
if (plan.ownedSessionCloseTarget) {
let closeResult;
try {
closeResult = await this.closeOwnedSessions(plan.ownedSessionCloseTarget === 'all'
? undefined
: plan.ownedSessionCloseTarget, {
removeClosedSessionsFromSandboxState: Boolean(plan.afterOwnedSessionClose
?.removeClosedSessionsFromSandboxState ||
plan.afterOwnedSessionClose?.clearSandboxState),
tracingParent: options.tracingParent,
});
}
catch (error) {
options.onCloseError();
throw error;
}
const { closedAgentKeys: closedSessionAgentKeys, closeErrors } = closeResult;
if (plan.afterOwnedSessionClose?.forgetLivePreservedSessions) {
forgetLivePreservedOwnedSessions(state);
}
if (plan.afterOwnedSessionClose?.removeFreshCreationSessionsFromSandboxState) {
const sandboxState = getSerializedSandboxState(state);
const freshCreationAgentKeys = new Set([...closedSessionAgentKeys].filter((agentKey) => sandboxState?.sessionsByAgent[agentKey]?.requiresFreshCreation ===
true));
if (freshCreationAgentKeys.size > 0) {
this.invalidateSerializedSessions(freshCreationAgentKeys);
}
}
if (plan.afterOwnedSessionClose?.removeClosedSessionsFromSandboxState) {
const sandboxState = getSerializedSandboxState(state);
if (sandboxState &&
!removeClosedSerializedSessions(sandboxState, closedSessionAgentKeys)) {
state._sandbox = undefined;
}
}
if (closeErrors.length > 0) {
options.onCloseError();
throw lifecycleErrorForCloseErrors('Failed to close one or more owned sandbox sessions.', closeErrors);
}
if (plan.afterOwnedSessionClose?.clearSandboxState) {
state._sandbox = undefined;
}
}
if (plan.deferredError) {
throw plan.deferredError;
}
}
async closeLivePreservedOwnedSessions(state, options = {}) {
const sessionAgentKeys = new Map();
for (const entry of livePreservedOwnedSessionEntries(state)) {
if (options.rejectedOnly && !entry.reuseRejected) {
continue;
}
const agentKeys = sessionAgentKeys.get(entry.session) ?? [];
agentKeys.push(entry.agentKey);
sessionAgentKeys.set(entry.session, agentKeys);
}
const closedAgentKeys = new Set();
const closeErrors = [];
for (const [session, agentKeys] of sessionAgentKeys) {
rejectLivePreservedOwnedSessionHandle({ state, session });
this.detachSessionAliases(session);
try {
await cleanupSandboxSession(session);
forgetLivePreservedOwnedSessionHandle({ state, session });
for (const agentKey of agentKeys) {
closedAgentKeys.add(agentKey);
}
}
catch (error) {
closeErrors.push(error);
}
}
const agentKeysWithLiveHandles = new Set(livePreservedOwnedSessionEntries(state).map((entry) => entry.agentKey));
for (const agentKey of agentKeysWithLiveHandles) {
closedAgentKeys.delete(agentKey);
}
return { closedAgentKeys, closeErrors };
}
async runPreStopHooksBeforeRelease() {
const shouldRunForOwnedSessions = Boolean(this.sandboxConfig?.client?.serializeSessionState);
const sessionsForPreStop = new Set();
const sessionsForHooks = new Set();
for (const [agentKey, session] of this.sessionsByAgentKey) {
if (!this.ownedSessionAgentKeys.has(agentKey)) {
sessionsForPreStop.add(session);
continue;
}
// Owned sessions without serialization run hooks through the close lifecycle.
if (shouldRunForOwnedSessions) {
sessionsForHooks.add(session);
}
}
for (const session of sessionsForPreStop) {
await runSandboxSessionPreStop(session);
}
for (const session of sessionsForHooks) {
if (!sessionsForPreStop.has(session)) {
await runSandboxSessionPreStopHooks(session);
}
}
}
async enqueueMemoryGeneration(state, args) {
if (!this.activeMemory || this.activeMemory.memory.generate === null) {
return;
}
try {
const manager = getOrCreateSandboxMemoryGenerationManager({
session: this.activeMemory.session,
memory: this.activeMemory.memory,
runAs: this.activeMemory.runAs,
runAgent: args.runAgent,
tracingParent: args.tracingParent,
});
await manager.enqueueState(state, {
exception: args.exception,
inputOverride: args.inputOverride,
rolloutIdentity: {
conversationId: state._conversationId ?? undefined,
sdkSessionId: await args.sdkSessionId?.(),
groupId: args.groupId,
},
});
}
catch (error) {
logModelAndToolActionWarning(logger, 'Failed to enqueue sandbox memory generation:', error);
}
}
async adoptPreservedOwnedSessions(tracingParent) {
// An explicit state replaces any preserved RunState entry. Do not inspect
// or resume that stale envelope before the replacement is processed. Signal
// callers to rehydrate any live tool closures against the explicit state.
if (this.sandboxConfig?.sessionState !== undefined) {
return true;
}
const sandboxState = getSerializedSandboxState(this.runState);
if (!hasPreservedOwnedSessions(sandboxState)) {
return false;
}
const client = this.sandboxConfig?.client;
if (!client) {
throw new UserError('Sandbox client must be configured to restore preserved sandbox sessions.');
}
if (sandboxState && sandboxState.backendId !== client.backendId) {
throw new UserError('RunState sandbox backend does not match the configured sandbox client.');
}
const preservedEntries = Object.entries(getPreviousSerializedSessionsByAgent(sandboxState, client)).filter(([, entry]) => entry.preservedOwnedSession);
let resumedSession = false;
for (const [agentKey] of preservedEntries) {
if (this.sessionsByAgentKey.has(agentKey)) {
continue;
}
const entry = getSerializedSessionEntryForAgent(getSerializedSandboxState(this.runState), agentKey);
if (!entry?.preservedOwnedSession) {
continue;
}
assertSessionStateEnvelope(client, entry.sessionState);
const trustedManifest = this.resolveTrustedManifestForAgentKey(agentKey);
const clientPreparesEnvironment = client.environmentPreparation === 'client';
const resumeTrustedManifest = clientPreparesEnvironment
? resolveTrustedManifestForResume(client, trustedManifest, this.sandboxConfig?.options)
: trustedManifest;
if (clientPreparesEnvironment && !resumeTrustedManifest) {
throw new UserError('Client-owned environment preparation requires a current trusted manifest before resuming a preserved sandbox session.');
}
const liveDecision = await this.inspectLivePreservedOwnedSessionReuse({
client,
agentKey,
serializedEntry: entry,
trustedManifest: resumeTrustedManifest,
trustedManifestAlreadyResolved: clientPreparesEnvironment,
});
if (entry.requiresFreshCreation === true) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
continue;
}
throw serializedOwnedSessionReplacementError(client);
}
if (liveDecision?.reusable) {
this.sessionsByAgentKey.set(agentKey, liveDecision.entry.session);
this.sessionAgentNamesByKey.set(agentKey, liveDecision.entry.currentAgentName);
this.ownedSessionAgentKeys.add(agentKey);
continue;
}
if (liveDecision?.mountAuthorityChanged) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
continue;
}
if (client.serializedSessionStateRequiresFreshCreation ||
serializedSessionEntryRequiresFreshMountAuthority(entry) ||
(resumeTrustedManifest &&
(manifestHasInContainerMounts(resumeTrustedManifest) ||
manifestHasNonResumableMountAuthority(resumeTrustedManifest)))) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
}
else {
this.invalidateSerializedSession(agentKey);
}
continue;
}
if (!client.resume) {
throw new UserError('Sandbox client must implement resume() to restore preserved sandbox sessions.');
}
const serializedState = await deserializeSandboxSessionStateEntry(client, entry, resumeTrustedManifest, {
clientOptions: this.sandboxConfig?.options,
snapshot: this.sandboxConfig?.snapshot,
}, { trustedManifestAlreadyResolved: clientPreparesEnvironment });
if (!serializedState) {
continue;
}
if (await serializedSessionStateRequiresFreshCreationForOptions(client, serializedState, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: resumeTrustedManifest }
: {}),
})) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
}
else {
throw serializedOwnedSessionReplacementError(client);
}
continue;
}
const rejectedLiveEntry = await this.prepareRejectedLiveSessionReplacement(client, liveDecision);
let session = await withSandboxSpan('sandbox.resume_session', {
agent_name: entry.currentAgentName,
backend_id: client.backendId,
}, async () => await resumeSandboxSession(client, serializedState, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: resumeTrustedManifest }
: {}),
}), tracingParent);
session = await this.replaceRejectedLiveSession(rejectedLiveEntry, session);
this.applyArchiveLimits(session);
this.sessionsByAgentKey.set(agentKey, session);
this.sessionAgentNamesByKey.set(agentKey, entry.currentAgentName);
this.ownedSessionAgentKeys.add(agentKey);
resumedSession = true;
}
return resumedSession;
}
acquireAgent(agent) {
const agentId = getObjectId(agent);
if (this.acquiredAgents.has(agentId)) {
return;
}
acquireSandboxAgent(agent);
this.acquiredAgents.set(agentId, agent);
}
async ensureSession(agent, tracingParent) {
const agentId = getObjectId(agent);
const agentKey = this.agentKey(agent);
const existing = this.sessionsByAgent.get(agentId);
if (existing) {
assertSandboxSessionStateUsable(existing.state);
this.currentAgentId = agentId;
return existing;
}
const existingByKey = this.sessionsByAgentKey.get(agentKey);
if (existingByKey) {
assertSandboxSessionStateUsable(existingByKey.state);
this.applyArchiveLimits(existingByKey);
await this.ensureSessionStarted(existingByKey, agent, 'resume', {
oncePerSession: true,
tracingParent,
});
this.currentAgentId = agentId;
this.sessionsByAgent.set(agentId, existingByKey);
this.sessionAgentNamesByKey.set(agentKey, agent.name);
return existingByKey;
}
if (this.sandboxConfig?.session) {
const session = this.sandboxConfig.session;
assertSandboxSessionStateUsable(session.state);
this.applyArchiveLimits(session);
const configuredManifest = this.resolveConfiguredManifest(agent, {
providedSession: session,
});
if (this.sandboxConfig.client?.environmentPreparation === 'client' &&
(manifestHasDeferredEnvironmentValues(configuredManifest.manifest) ||
manifestHasDeferredEnvironmentValues(session.state.manifest))) {
throw new UserError('Client-owned environment preparation cannot revalidate deferred environment values for an already-running provided session. Resume the session through the client instead.');
}
// Provided sessions are already running, so only a safe additive manifest delta can
// be applied instead of reprovisioning root, env, users, groups, or mounts.
await applyManifestToProvidedSession(session, configuredManifest.manifest, sandboxRunAsName(agent.runAs));
await this.ensureSessionStarted(session, agent, 'provided', {
oncePerSession: true,
tracingParent,
});
this.registerSessionForAgent(agent, session);
return session;
}
const configuredManifest = this.resolveConfiguredManifest(agent);
validateMountCredentialBoundaries(configuredManifest.manifest);
const client = this.requireClient();
const resumed = await this.resumeSessionForAgent(client, agent, configuredManifest.manifest, tracingParent);
if (resumed) {
this.applyArchiveLimits(resumed);
await this.ensureSessionStarted(resumed, agent, 'resume', {
tracingParent,
});
this.registerSessionForAgent(agent, resumed, { owned: true });
return resumed;
}
if (!client.create) {
throw new UserError('Sandbox execution requires a sandbox client with create() support.');
}
const clientPreparesEnvironment = client.environmentPreparation === 'client';
if (clientPreparesEnvironment) {
validateMountEnvironmentCredentialBoundaries(configuredManifest.manifest, {}, { preserveDeferredValuePresence: true });
}
const createManifest = clientPreparesEnvironment
? cloneManifest(configuredManifest.manifest)
: await resolveAndValidateMountEnvironment(configuredManifest.manifest);
const createSession = client.create.bind(client);
const createArgs = {
snapshot: this.resolveSnapshotSpec(client),
options: this.sandboxConfig?.options,
concurrencyLimits: this.sandboxConfig?.concurrencyLimits,
archiveLimits: this.sandboxConfig?.archiveLimits,
};
if (configuredManifest.passToCreate) {
createArgs.manifest = createManifest;
}
const session = await withSandboxSpan('sandbox.create_session', {
agent_name: agent.name,
backend_id: client.backendId,
}, async () => await createSession(createArgs), tracingParent);
if (clientPreparesEnvironment) {
bindClientPreparedTrustedManifest(session, configuredManifest.manifest);
}
else if (configuredManifest.passToCreate) {
session.state.environment = {
...(session.state.environment ?? {}),
...(await createManifest.resolveEnvironment()),
};
const manifestWithTrustedPolicies = manifestWithTrustedRuntimePolicies(session.state.manifest, configuredManifest.manifest);
recordLiveMountCredentialAuthority(manifestWithTrustedPolicies, session.state.manifest);
session.state.manifest = manifestWithTrustedPolicies;
}
this.applyArchiveLimits(session);
await this.ensureSessionStarted(session, agent, 'create', {
tracingParent,
});
this.registerSessionForAgent(agent, session, { owned: true });
return session;
}
applyArchiveLimits(session) {
if (this.sandboxConfig?.archiveLimits === undefined) {
return;
}
session.setArchiveLimits?.(this.sandboxConfig.archiveLimits);
}
registerSessionForAgent(agent, session, options = {}) {
const agentId = getObjectId(agent);
const agentKey = this.agentKey(agent);
this.currentAgentId = agentId;
this.sessionsByAgent.set(agentId, session);
this.sessionsByAgentKey.set(agentKey, session);
this.sessionAgentNamesByKey.set(agentKey, agent.name);
if (options.owned) {
this.ownedSessionAgentKeys.add(agentKey);
captureLiveMountCredentialAuthorityIfAbsent(session.state.manifest, session.state.environment);
}
}
async ensureSessionStarted(session, agent, reason, options = {}) {
if (!session.start) {
return;
}
if (options.oncePerSession) {
// Provided and resumed sessions may be shared by multiple agents in one run; keep
// their provider-specific start hook idempotent.
const started = this.sessionStartPromises.get(session);
if (started) {
await started;
return;
}
}
if (session.running && (await session.running())) {
if (options.oncePerSession) {
this.sessionStartPromises.set(session, Promise.resolve());
}
return;
}
const startPromise = withSandboxSpan('sandbox.start', {
agent_name: agent.name,
}, async () => {
await session.start({ reason });
}, options.tracingParent);
if (options.oncePerSession) {
this.sessionStartPromises.set(session, startPromise);
}
try {
await startPromise;
}
catch (error) {
if (options.oncePerSession) {
this.sessionStartPromises.delete(session);
}
throw error;
}
}
getPreparedAgent(agent, session, runConfigModel, tracingParent) {
const agentId = getObjectId(agent);
const manifestSignature = getManifestSignature(session.state.manifest);
const cached = this.preparedAgents.get(agentId);
if (cached &&
this.preparedSessions.get(agentId) === session &&
this.preparedManifestSignatures.get(agentId) === manifestSignature) {
const cachedSandboxAgent = cached;
for (const capability of cachedSandboxAgent.capabilities) {
capability.bindTracingParent(tracingParent);
}
refreshPreparedAgentTools(cachedSandboxAgent);
return cachedSandboxAgent;
}
// Capability instructions include a rendered filesystem view, so a manifest change
// invalidates the prepared-agent cache even when the live session object is unchanged.
const prepared = prepareSandboxAgent({
agent,
session,
capabilities: cloneSandboxCapabilities(agent.capabilities),
runConfigModel,
processManifest: false,
tracingParent,
workspaceScope: this.workspaceScope,
});
this.preparedAgents.set(agentId, prepared);
this.preparedSessions.set(agentId, session);
this.preparedManifestSignatures.set(agentId, manifestSignature);
return prepared;
}
async ensureWorkspaceScopeAccessible(session, runAs) {
const cwd = this.workspaceScope.cwd;
if (cwd === undefined) {
return;
}
if (!session.directoryExists) {
throw new UserError('Sandbox sessions used with sandbox.cwd must provide directoryExists().');
}
const accessible = await session.directoryExists(cwd, runAs);
if (!accessible) {
throw new UserError(`Sandbox working directory "${cwd}" does not exist or is not accessible.`);
}
}
async resumeSessionForAgent(client, agent, trustedManifest, tracingParent) {
const agentKey = this.agentKey(agent);
const serializedEntry = getSerializedSessionEntryForAgent(getSerializedSandboxState(this.runState), agentKey);
const explicitSessionStateInput = this.sandboxConfig?.sessionState;
const hasExplicitSessionState = explicitSessionStateInput !== undefined;
if (!hasExplicitSessionState && serializedEntry) {
assertSessionStateEnvelope(client, serializedEntry.sessionState);
}
const clientPreparesEnvironment = client.environmentPreparation === 'client';
const resumeTrustedManifest = clientPreparesEnvironment &&
(hasExplicitSessionState || serializedEntry !== undefined)
? resolveTrustedManifestForResume(client, trustedManifest, this.sandboxConfig?.options)
: trustedManifest;
const explicitTrustedManifest = explicitSessionStateInput
? resumeTrustedManifest
: trustedManifest;
if (explicitSessionStateInput) {
client.validateSessionStateForResume?.({ source: 'explicit', state: explicitSessionStateInput }, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: explicitTrustedManifest }
: {}),
});
}
const explicitResume = hasExplicitSessionState
? await this.prepareExplicitSessionStateForResume(client, trustedManifest, clientPreparesEnvironment ? explicitTrustedManifest : undefined)
: undefined;
const explicitSessionState = explicitResume?.state;
if (explicitSessionState && !client.resume) {
throw new UserError('Sandbox client must implement resume() to restore sandbox session state.');
}
const explicitClientPreparedLiveEntry = hasExplicitSessionState && clientPreparesEnvironment
? livePreservedOwnedSession({
runState: this.runState,
client,
agentKey,
serializedEntry,
})
: undefined;
const liveDecision = hasExplicitSessionState && clientPreparesEnvironment
? explicitClientPreparedLiveEntry
? { entry: explicitClientPreparedLiveEntry, reusable: false }
: undefined
: await this.inspectLivePreservedOwnedSessionReuse({
client,
agentKey,
serializedEntry,
trustedManifest: resumeTrustedManifest,
trustedManifestAlreadyResolved: clientPreparesEnvironment,
});
if (!hasExplicitSessionState &&
serializedEntry?.requiresFreshCreation === true) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
return undefined;
}
throw serializedOwnedSessionReplacementError(client);
}
if (!hasExplicitSessionState && liveDecision?.reusable) {
return liveDecision.entry.session;
}
if (!hasExplicitSessionState && liveDecision?.mountAuthorityChanged) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
return undefined;
}
if (!hasExplicitSessionState &&
(client.serializedSessionStateRequiresFreshCreation ||
manifestHasInContainerMounts(resumeTrustedManifest) ||
manifestHasNonResumableMountAuthority(resumeTrustedManifest) ||
serializedSessionEntryRequiresFreshMountAuthority(serializedEntry))) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
}
else {
this.invalidateSerializedSession(agentKey);
}
return undefined;
}
if (!client.resume) {
if (this.sandboxConfig?.sessionState || serializedEntry) {
throw new UserError('Sandbox client must implement resume() to restore sandbox session state.');
}
return undefined;
}
if (explicitSessionState) {
const rejectedLiveEntry = await this.prepareRejectedLiveSessionReplacement(client, liveDecision ? { ...liveDecision, reusable: false } : undefined);
const session = await withSandboxSpan('sandbox.resume_session', {
agent_name: agent.name,
backend_id: client.backendId,
}, async () => await resumeSandboxSession(client, explicitSessionState, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: explicitResume?.trustedManifest }
: {}),
}), tracingParent);
return await this.replaceRejectedLiveSession(rejectedLiveEntry, session);
}
const serializedState = await deserializeSandboxSessionStateEntry(client, serializedEntry, resumeTrustedManifest, {
clientOptions: this.sandboxConfig?.options,
snapshot: this.sandboxConfig?.snapshot,
}, { trustedManifestAlreadyResolved: clientPreparesEnvironment });
if (!serializedState) {
return undefined;
}
if (await serializedSessionStateRequiresFreshCreationForOptions(client, serializedState, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: resumeTrustedManifest }
: {}),
})) {
if (liveDecision) {
await this.discardLiveSessionWithChangedMountAuthority(liveDecision);
}
else {
throw serializedOwnedSessionReplacementError(client);
}
return undefined;
}
const rejectedLiveEntry = await this.prepareRejectedLiveSessionReplacement(client, liveDecision);
const session = await withSandboxSpan('sandbox.resume_session', {
agent_name: agent.name,
backend_id: client.backendId,
}, async () => await resumeSandboxSession(client, serializedState, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(clientPreparesEnvironment
? { trustedManifest: resumeTrustedManifest }
: {}),
}), tracingParent);
return await this.replaceRejectedLiveSession(rejectedLiveEntry, session);
}
async prepareExplicitSessionStateForResume(client, trustedManifest, preResolvedTrustedManifest) {
const persistedState = this.sandboxConfig.sessionState;
const trustedManifestForPolicy = preResolvedTrustedManifest ?? trustedManifest;
if (client.serializedSessionStateRequiresFreshCreation ||
manifestHasInContainerMounts(trustedManifestForPolicy) ||
manifestHasNonResumableMountAuthority(trustedManifestForPolicy) ||
manifestHasInContainerMounts(persistedState.manifest) ||
manifestHasNonResumableMountAuthority(persistedState.manifest)) {
throw new UserError(client.serializedSessionStateRequiresFreshCreation
? 'This sandbox provider cannot safely resume persisted session state. Create a fresh sandbox from current trusted configuration.'
: 'Sandbox session state with in-container mounts cannot be resumed safely. Create a fresh sandbox from the current trusted manifest.');
}
const resolvedTrustedManifest = preResolvedTrustedManifest ??
resolveTrustedManifestForResume(client, trustedManifest, this.sandboxConfig?.options);
if (client.backendId === 'docker' &&
client.environmentPreparation !== 'client' &&
Object.keys(persistedState.manifest.environment).some((key) => !(key in trustedManifestForPolicy.environment))) {
throw new UserError('Docker sandbox session state cannot be resumed because the current trusted manifest removes an environment variable. Start a fresh session from a snapshot instead.');
}
const topologyReboundState = rebindPersistedMountCredentials(persistedState, resolvedTrustedManifest);
assertMountCredentialsRebound(topologyReboundState);
if (!liveMountCredentialAuthorityMatches(persistedState.manifest, resolvedTrustedManifest)) {
throw new SandboxMountError('Sandbox session state mount authority does not match the current trusted manifest. Create a fresh sandbox from current trusted configuration.', undefined, 'mount_config_invalid');
}
if (client.environmentPreparation === 'client') {
validateMountCredentialBoundaries(resolvedTrustedManifest);
validateMountEnvironmentCredentialBoundaries(resolvedTrustedManifest, {}, { preserveDeferredValuePresence: true });
const pathReboundState = rebindPersistedPathGrants(topologyReboundState, resolvedTrustedManifest, {
replaceWithTrustedManifest: client.backendId === 'docker',
});
assertMountCredentialsRebound(pathReboundState);
assertHostPathGrantsRebound(pathReboundState);
return {
state: persistedState,
trustedManifest: resolvedTrustedManifest,
};
}
const materializedTrustedManifest = await resolveAndValidateMountEnvironment(resolvedTrustedManifest);
const credentialReboundState = rebindPersistedMountCredentials(topologyReboundState, materializedTrustedManifest);
const sessionState = rebindPersistedPathGrants(credentialReboundState, materializedTrustedManifest, {
replaceWithTrustedManifest: client.backendId === 'docker',
});
if (client.backendId === 'docker') {
sessionState.environment = {
...(sessionState.environment ?? {}),
...(await materializedTrustedManifest.resolveEnvironment()),
};
}
assertMountCredentialsRebound(sessionState);
validateMountCredentialBoundaries(sessionState.manifest);
validateMountEnvironmentCredentialBoundaries(sessionState.manifest, sessionState.environment ?? {});
assertHostPathGrantsRebound(sessionState);
return {
state: sessionState,
trustedManifest: materializedTrustedManifest,
};
}
async inspectLivePreservedOwnedSessionReuse(args) {
const liveEntry = livePreservedOwnedSession({
runState: this.runState,
client: args.client,
agentKey: args.agentKey,
serializedEntry: args.serializedEntry,
});
if (!liveEntry) {
return undefined;
}
const trustedManifest = args.trustedManifestAlreadyResolved
? args.trustedManifest
: resolveTrustedManifestForResume(args.client, args.trustedManifest, this.sandboxConfig?.options);
assertTrustedManifestForDockerRunState(args.client, trustedManifest);
return await withExclusiveProvidedSessionManifestMutation(liveEntry.session.state, async () => await withExclusiveSandboxStateInspection(liveEntry.session.state, async () => {
if (liveEntry.reuseRejected) {
await serializedSessionStateRequiresFreshCreationForOptions(args.client, liveEntry.session.state, {
archiveLimits: this.sandboxConfig?.archiveLimits,
clientOptions: this.sandboxConfig?.options,
...(args.client.environmentPreparation === 'client'
? { trustedManifest }
: {}),
});
return {
entry: liveEntry,
reusable: false,
...(args.client
.preservedOwnedSessionReuseRejectionRequiresFreshCreation ===
true
? { mountAuthorityChanged: true }
: {}),
};
}
if (isSandboxSessionStateUnsafe(liveEntry.session.state)) {
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
mountAuthorityChanged: true,
};
}
if (!trustedManifest ||
manifestHasNonResumableMountAuthority(trustedManifest) ||
!liveMountCredentialAuthorityMatches(liveEntry.session.state.manifest, trustedManifest)) {
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
mountAuthorityChanged: true,
};
}
const canReuse = args.client.canReusePreservedOwnedSession;
const clientPreparesEnvironment = args.client.environmentPreparation === 'client';
if (args.client.backendId === 'docker' &&
!clientPreparesEnvironment &&
Object.keys(liveEntry.session.state.manifest.environment).some((key) => !(key in trustedManifest.environment))) {
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return { entry: liveEntry, reusable: false };
}
const hasDeferredEnvironmentValues = manifestHasDeferredEnvironmentValues(trustedManifest) ||
manifestHasDeferredEnvironmentValues(liveEntry.session.state.manifest);
if (clientPreparesEnvironment) {
validateMountCredentialBoundaries(trustedManifest);
validateMountEnvironmentCredentialBoundaries(trustedManifest, {}, {
preserveDeferredValuePresence: true,
});
const hasOtherMappedAlias = [...this.sessionsByAgentKey].some(([mappedAgentKey, mappedSession]) => mappedAgentKey !== args.agentKey &&
mappedSession === liveEntry.session);
if (hasOtherMappedAlias &&
!clientPreparedManifestAuthoritiesMatch(liveEntry.session.state.manifest, liveEntry.session.state.manifest, trustedManifest)) {
const requiresFreshCreation = args.client
.preservedOwnedSessionReuseRejectionRequiresFreshCreation ===
true;
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
...(requiresFreshCreation
? { mountAuthorityChanged: true }
: {}),
};
}
if (!canReuse) {
const authorityMatches = !hasDeferredEnvironmentValues &&
clientPreparedManifestAuthoritiesMatch(liveEntry.session.state.manifest, liveEntry.session.state.manifest, trustedManifest);
if (!authorityMatches) {
const requiresFreshCreation = args.client
.preservedOwnedSessionReuseRejectionRequiresFreshCreation ===
true;
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
...(requiresFreshCreation
? { mountAuthorityChanged: true }
: {}),
};
}
}
else {
const reusable = await canReuse.call(args.client, liveEntry.session.state, {
clientOptions: this.sandboxConfig?.options,
revalidateManifestEntries: true,
trustedManifest,
});
if (!reusable) {
const requiresFreshCreation = args.client
.preservedOwnedSessionReuseRejectionRequiresFreshCreation ===
true;
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
...(requiresFreshCreation
? { mountAuthorityChanged: true }
: {}),
};
}
}
args.client.rebindPreservedOwnedSessionState?.(liveEntry.session.state, {
clientOptions: this.sandboxConfig?.options,
revalidateManifestEntries: true,
trustedManifest,
});
bindClientPreparedTrustedManifest(liveEntry.session, trustedManifest);
return { entry: liveEntry, reusable: true };
}
const candidateState = rebindPersistedPathGrants(liveEntry.session.state, trustedManifest, {
replaceWithTrustedManifest: args.client.backendId === 'docker',
});
replaceManifestMountCredentialExposurePolicy(candidateState.manifest, trustedManifest);
recordLiveMountCredentialAuthority(candidateState.manifest, liveEntry.session.state.manifest);
validateMountCredentialBoundaries(trustedManifest);
if (args.client.backendId === 'docker') {
candidateState.environment = {
...(liveEntry.session.state.environment ?? {}),
...(await trustedManifest.resolveEnvironment()),
};
}
if (!canReuse) {
const trustedEnvironment = await trustedManifest.resolveEnvironment();
if (!liveMountEnvironmentAuthorityMatches(liveEntry.session.state.manifest, trustedManifest, trustedEnvironment)) {
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
mountAuthorityChanged: true,
};
}
await refreshLiveEnvironmentReferences(candidateState, trustedManifest, trustedEnvironment);
}
else {
const reusable = await canReuse.call(args.client, candidateState, {
clientOptions: this.sandboxConfig?.options,
revalidateManifestEntries: true,
trustedManifest,
});
if (!reusable) {
const requiresFreshCreation = args.client
.preservedOwnedSessionReuseRejectionRequiresFreshCreation ===
true;
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: liveEntry.session,
});
return {
entry: liveEntry,
reusable: false,
...(requiresFreshCreation
? { mountAuthorityChanged: true }
: {}),
};
}
if (args.client.backendId !== 'docker') {
await refreshLiveEnvironmentReferences(candidateState, trustedManifest);
}
}
args.client.rebindPreservedOwnedSessionState?.(liveEntry.session.state, {
clientOptions: this.sandboxConfig?.options,
revalidateManifestEntries: true,
trustedManifest,
});
// Rebind only after the backend has verified that its live authority still
// matches the current trusted manifest.
const reboundManifest = args.client.backendId === 'docker'
? manifestWithTrustedRuntimePolicies(rebindPersistedPathGrants(liveEntry.session.state, trustedManifest, {
replaceWithTrustedGrantSet: true,
}).manifest, trustedManifest)
: candidateState.manifest;
recordLiveMountCredentialAuthority(reboundManifest, candidateState.manifest);
liveEntry.session.state.manifest = reboundManifest;
liveEntry.session.state.environment = candidateState.environment;
return { entry: liveEntry, reusable: true };
}));
}
async discardLiveSessionWithChangedMountAuthority(decision) {
const session = decision.entry.session;
const agentKeys = new Set([decision.entry.agentKey]);
for (const entry of livePreservedOwnedSessionEntries(this.runState)) {
if (entry.session === session) {
agentKeys.add(entry.agentKey);
}
}
for (const agentKey of this.detachSessionAliases(session)) {
agentKeys.add(agentKey);
}
this.markSerializedSessionsRequireFreshCreation(agentKeys);
for (const agentKey of agentKeys) {
this.sessionsByAgentKey.delete(agentKey);
this.sessionAgentNamesByKey.delete(agentKey);
this.ownedSessionAgentKeys.delete(agentKey);
}
await cleanupSandboxSession(decision.entry.session);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session,
});
this.invalidateSerializedSessions(agentKeys);
}
detachSessionAliases(session) {
const detachedAgentKeys = new Set();
for (const [agentKey, mappedSession] of this.sessionsByAgentKey) {
if (mappedSession !== session) {
continue;
}
detachedAgentKeys.add(agentKey);
this.sessionsByAgentKey.delete(agentKey);
this.sessionAgentNamesByKey.delete(agentKey);
this.ownedSessionAgentKeys.delete(agentKey);
}
for (const [agentId, mappedSession] of this.sessionsByAgent) {
if (mappedSession !== session) {
continue;
}
this.sessionsByAgent.delete(agentId);
if (this.currentAgentId === agentId) {
this.currentAgentId = undefined;
}
}
for (const [agentId, preparedSession] of this.preparedSessions) {
if (preparedSession !== session) {
continue;
}
this.preparedSessions.delete(agentId);
this.preparedAgents.delete(agentId);
this.preparedManifestSignatures.delete(agentId);
}
if (this.activeMemory?.session === session) {
this.activeMemory = undefined;
}
return detachedAgentKeys;
}
invalidateSerializedSession(agentKey) {
this.invalidateSerializedSessions(new Set([agentKey]));
}
markSerializedSessionsRequireFreshCreation(agentKeys) {
const sandboxState = getSerializedSandboxState(this.runState);
if (!sandboxState) {
return;
}
for (const agentKey of agentKeys) {
const entry = sandboxState.sessionsByAgent[agentKey];
if (!entry) {
continue;
}
entry.preservedOwnedSession = true;
entry.reuseLiveSession = false;
entry.requiresFreshCreation = true;
}
}
invalidateSerializedSessions(agentKeys) {
const sandboxState = getSerializedSandboxState(this.runState);
const agentKeysWithLiveHandles = new Set(this.runState
? livePreservedOwnedSessionEntries(this.runState).map((entry) => entry.agentKey)
: []);
const removableAgentKeys = new Set([...agentKeys].filter((agentKey) => !agentKeysWithLiveHandles.has(agentKey)));
if (sandboxState &&
removableAgentKeys.size > 0 &&
!removeClosedSerializedSessions(sandboxState, removableAgentKeys)) {
this.runState._sandbox = undefined;
}
}
async prepareRejectedLiveSessionReplacement(client, decision) {
if (!decision || decision.reusable) {
return undefined;
}
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session: decision.entry.session,
});
if (client.backendId === 'docker') {
return decision.entry;
}
this.detachSessionAliases(decision.entry.session);
await cleanupSandboxSession(decision.entry.session);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: decision.entry.session,
});
return undefined;
}
async replaceRejectedLiveSession(rejectedEntry, replacement) {
if (!rejectedEntry) {
return replacement;
}
rememberRejectedLivePreservedOwnedSessionHandle({
state: this.runState,
source: rejectedEntry,
session: replacement,
});
this.detachSessionAliases(rejectedEntry.session);
try {
await cleanupSandboxSession(rejectedEntry.session);
}
catch (rejectedCleanupError) {
try {
await cleanupSandboxSession(replacement);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: replacement,
});
}
catch (replacementCleanupError) {
throw new SandboxLifecycleError('Failed to close the rejected live sandbox session and its replacement.', {
errors: [rejectedCleanupError, replacementCleanupError],
});
}
throw rejectedCleanupError;
}
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: rejectedEntry.session,
});
const siblingRejectedSessions = new Set(this.runState
? livePreservedOwnedSessionEntries(this.runState)
.filter((entry) => entry.reuseRejected &&
entry.agentKey === rejectedEntry.agentKey &&
entry.session !== replacement)
.map((entry) => entry.session)
: []);
const siblingCleanupErrors = [];
for (const siblingSession of siblingRejectedSessions) {
this.detachSessionAliases(siblingSession);
try {
await cleanupSandboxSession(siblingSession);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: siblingSession,
});
}
catch (error) {
siblingCleanupErrors.push(error);
}
}
if (siblingCleanupErrors.length > 0) {
try {
await cleanupSandboxSession(replacement);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: replacement,
});
}
catch (replacementCleanupError) {
siblingCleanupErrors.push(replacementCleanupError);
}
throw lifecycleErrorForCloseErrors('Failed to close rejected live sandbox session handles and their replacement.', siblingCleanupErrors);
}
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session: replacement,
});
return replacement;
}
async serializeState(args = {}) {
const currentAgent = this.currentAgentId
? this.acquiredAgents.get(this.currentAgentId)
: undefined;
const sandboxState = getSerializedSandboxState(this.runState);
const preferredCurrentAgentKey = currentAgent
? this.agentKey(currentAgent)
: sandboxState?.currentAgentKey;
const retainedRejectedAgentKeys = new Set(this.runState
? livePreservedOwnedSessionEntries(this.runState)
.filter((entry) => entry.reuseRejected)
.map((entry) => entry.agentKey)
: []);
return await serializeSandboxRuntimeState({
client: this.sandboxConfig?.client,
sandboxState,
sessionsByAgentKey: this.sessionsByAgentKey,
sessionAgentNamesByKey: this.sessionAgentNamesByKey,
ownedSessionAgentKeys: this.ownedSessionAgentKeys,
trustedManifestForAgentKey: (agentKey) => resolveTrustedManifestForResume(this.sandboxConfig.client, this.resolveTrustedManifestForAgentKey(agentKey, this.ownedSessionAgentKeys.has(agentKey)
? undefined
: this.sessionsByAgentKey.get(agentKey)), this.sandboxConfig?.options),
clientOptions: this.sandboxConfig?.options,
includeOwnedSessions: args.includeOwnedSessions,
preferredCurrentAgentKey,
retainedPreservedOwnedSessionAgentKeys: retainedRejectedAgentKeys,
});
}
requireClient() {
if (!this.sandboxConfig?.client) {
throw new UserError('Sandbox execution requires `RunConfig.sandbox.client` unless a live session is provided.');
}
return this.sandboxConfig.client;
}
resolveSnapshotSpec(client) {
if (this.sandboxConfig?.snapshot) {
return this.sandboxConfig.snapshot;
}
if (!client.supportsDefaultOptions) {
return undefined;
}
return { type: 'local' };
}
agentKey(agent) {
const agentId = getObjectId(agent);
const existing = this.agentKeys.get(agentId);
if (existing) {
this.agentsByKey.set(existing, agent);
return existing;
}
const agentKey = this.allocateRuntimeAgentKey(agent.name);
this.agentKeys.set(agentId, agentKey);
this.agentsByKey.set(agentKey, agent);
return agentKey;
}
allocateRuntimeAgentKey(agentName) {
const usedKeys = new Set(this.agentKeys.values());
if (!usedKeys.has(agentName)) {
return agentName;
}
let suffix = 2;
while (usedKeys.has(`${agentName}_${suffix}`)) {
suffix += 1;
}
return `${agentName}_${suffix}`;
}
resolveConfiguredManifest(agent, options = {}) {
const baseManifest = this.sandboxConfig?.manifest ??
agent.defaultManifest ??
options.providedSession?.state.manifest;
const initialManifest = baseManifest
? cloneManifest(baseManifest)
: new Manifest();
const manifestWithIdentity = options.providedSession
? initialManifest
: manifestWithRunAsUser(initialManifest, agent.runAs);
const configuredManifest = cloneSandboxCapabilities(agent.capabilities).reduce((manifest, capability) => capability.processManifest(manifest), manifestWithIdentity);
// Passing a truly default manifest to providers can override their natural root
// defaults, so create() receives a manifest only when configuration changed it.
return {
manifest: configuredManifest,
passToCreate: baseManifest !== undefined || !isDefaultManifest(configuredManifest),
};
}
resolveTrustedManifestForAgentKey(agentKey, providedSession) {
const agent = this.agentsByKey.get(agentKey);
if (!agent || !isSandboxAgent(agent)) {
return undefined;
}
return this.resolveConfiguredManifest(agent, { providedSession }).manifest;
}
async closeOwnedSessions(agentKeys, options = {}) {
const keysToClose = [...(agentKeys ?? this.ownedSessionAgentKeys)].filter((agentKey) => this.ownedSessionAgentKeys.has(agentKey));
const sessionsToClose = new Map();
for (const agentKey of keysToClose) {
const session = this.sessionsByAgentKey.get(agentKey);
if (!session || !hasSessionCleanup(session)) {
continue;
}
const existing = sessionsToClose.get(session);
if (existing) {
existing.agentKeys.push(agentKey);
}
else {
sessionsToClose.set(session, {
agentKeys: [agentKey],
agentName: this.sessionAgentNamesByKey.get(agentKey),
});
}
}
if (sessionsToClose.size === 0) {
return { closedAgentKeys: new Set(), closeErrors: [] };
}
const closedAgentKeys = new Set();
const closeErrors = [];
await withSandboxSpan('sandbox.cleanup_sessions', {
session_count: sessionsToClose.size,
}, async (cleanupSessionsSpan) => {
const sessions = [...sessionsToClose];
const results = await Promise.allSettled(sessions.map(async ([session, { agentName }]) => {
await withSandboxSpan('sandbox.shutdown', {
agent_name: agentName,
}, async () => {
rejectLivePreservedOwnedSessionHandle({
state: this.runState,
session,
});
await cleanupSandboxSession(session);
forgetLivePreservedOwnedSessionHandle({
state: this.runState,
session,
});
}, cleanupSessionsSpan);
}));
for (const [index, result] of results.entries()) {
const [session, { agentKeys }] = sessions[index];
if (result.status === 'rejected') {
closeErrors.push(result.reason);
continue;
}
const closedKeys = new Set(agentKeys);
if (options.removeClosedSessionsFromSandboxState) {
this.invalidateSerializedSessions(closedKeys);
}
for (const agentKey of closedKeys) {
this.sessionsByAgentKey.delete(agentKey);
this.sessionAgentNamesByKey.delete(agentKey);
this.ownedSessionAgentKeys.delete(agentKey);
closedAgentKeys.add(agentKey);
}
for (const [agentId, mappedSession] of this.sessionsByAgent) {
if (mappedSession === session) {
this.sessionsByAgent.delete(agentId);
if (this.currentAgentId === agentId) {
this.currentAgentId = undefined;
}
}
}
for (const [agentId, preparedSession] of this.preparedSessions) {
if (preparedSession === session) {
this.preparedSessions.delete(agentId);
this.preparedAgents.delete(agentId);
this.preparedManifestSignatures.delete(agentId);
}
}
if (this.activeMemory?.session === session) {
this.activeMemory = undefined;
}
}
}, options.tracingParent);
return { closedAgentKeys, closeErrors };
}
releaseAgents() {
releaseSandboxAgents(this.acquiredAgents.values());
this.acquiredAgents.clear();
}
}
function getManifestSignature(manifest) {
return stableJsonStringify({
version: manifest.version,
root: manifest.root,
entries: manifest.entries,
environment: serializeManifestEnvironment(manifest),
users: manifest.users,
groups: manifest.groups,
extraPathGrants: manifest.extraPathGrants,
remoteMountCommandAllowlist: manifest.remoteMountCommandAllowlist,
mountCredentialExposure: manifest.mountTargets().map(({ mountPath }) => ({
mountPath,
mountScoped: manifestAcknowledgesInContainerMountCredentialExposure(manifest, mountPath, 'mount_scoped'),
broad: manifestAcknowledgesInContainerMountCredentialExposure(manifest, mountPath, 'broad'),
})),
});
}
function isDefaultManifest(manifest) {
const defaultManifest = new Manifest();
return (manifest.version === defaultManifest.version &&
manifest.root === defaultManifest.root &&
Object.keys(manifest.entries).length === 0 &&
Object.keys(manifest.environment).length === 0 &&
manifest.users.length === 0 &&
manifest.groups.length === 0 &&
manifest.extraPathGrants.length === 0 &&
isDefaultRemoteMountCommandAllowlist(manifest.remoteMountCommandAllowlist));
}
function manifestWithTrustedRuntimePolicies(manifest, trustedManifest) {
const merged = new Manifest({
version: manifest.version,
root: manifest.root,
entries: structuredClone(manifest.entries),
environment: Object.fromEntries(Object.entries(trustedManifest.environment).map(([key, value]) => [
key,
value.init(),
])),
users: structuredClone(manifest.users),
groups: structuredClone(manifest.groups),
extraPathGrants: structuredClone(manifest.extraPathGrants),
remoteMountCommandAllowlist: [
...trustedManifest.remoteMountCommandAllowlist,
],
});
replaceManifestMountCredentialExposurePolicy(merged, trustedManifest);
return merged;
}
function manifestHasDeferredEnvironmentValues(manifest) {
return Object.values(manifest.environment).some((value) => isEnvValueReference(value) || value.resolver !== undefined);
}
async function resumeSandboxSession(client, state, options) {
if (client.environmentPreparation !== 'client') {
return await client.resume(state, options);
}
const session = await withExclusiveProvidedSessionManifestMutation(state, () => withExclusiveSandboxStateInspection(state, async () => {
const previousManifest = state.manifest;
const rebound = rebindPersistedPathGrants(rebindPersistedMountCredentials(state, options.trustedManifest), options.trustedManifest, { replaceWithTrustedManifest: client.backendId === 'docker' });
assertMountCredentialsRebound(rebound);
assertHostPathGrantsRebound(rebound);
// Preserve branded provider state while restoring SDK-owned authority before
// the provider can use it. A failed resume must leave the input retryable.
state.manifest = rebound.manifest;
try {
const resumed = await client.resume(state, options);
if (resumed.state === state) {
bindClientPreparedTrustedManifest(resumed, options.trustedManifest);
}
return resumed;
}
catch (error) {
state.manifest = previousManifest;
throw error;
}
}));
if (session.state !== state) {
// Do not hold the input lock while acquiring a distinct returned-state lock.
await withExclusiveProvidedSessionManifestMutation(session.state, () => withExclusiveSandboxStateInspection(session.state, async () => {
bindClientPreparedTrustedManifest(session, options.trustedManifest);
}));
}
return session;
}
function bindClientPreparedTrustedManifest(session, trustedManifest) {
const runnerManifest = mergeClientPreparedManifestAuthority(session.state.manifest, trustedManifest);
recordLiveMountCredentialAuthority(runnerManifest, trustedManifest);
session.state.manifest = runnerManifest;
}
async function refreshLiveEnvironmentReferences(state, trustedManifest, resolvedTrustedEnvironment) {
if (!trustedManifest) {
return;
}
const referenceKeys = new Set([
...Object.entries(state.manifest.environment)
.filter(([, value]) => isEnvValueReference(value))
.map(([key]) => key),
...Object.entries(trustedManifest.environment)
.filter(([, value]) => isEnvValueReference(value))
.map(([key]) => key),
]);
if (referenceKeys.size === 0) {
return;
}
const resolvedEnvironment = Object.fromEntries((await Promise.all([...referenceKeys].map(async (key) => {
const value = trustedManifest.environment[key];
return value
? [
key,
resolvedTrustedEnvironment?.[key] ?? (await value.resolve()),
]
: undefined;
}))).filter((entry) => entry !== undefined));
const environment = { ...(state.environment ?? {}) };
const manifestEnvironment = Object.fromEntries(Object.entries(state.manifest.environment)
.filter(([key]) => !referenceKeys.has(key))
.map(([key, value]) => [key, value.init()]));
for (const key of referenceKeys) {
delete environment[key];
const value = trustedManifest.environment[key];
if (value) {
manifestEnvironment[key] = value.init();
}
}
const manifest = new Manifest({
version: state.manifest.version,
root: state.manifest.root,
entries: structuredClone(state.manifest.entries),
environment: manifestEnvironment,
users: structuredClone(state.manifest.users),
groups: structuredClone(state.manifest.groups),
extraPathGrants: structuredClone(state.manifest.extraPathGrants),
remoteMountCommandAllowlist: [
...state.manifest.remoteMountCommandAllowlist,
],
});
replaceManifestMountCredentialExposurePolicy(manifest, state.manifest);
recordLiveMountCredentialAuthority(manifest, state.manifest);
state.environment = {
...environment,
...resolvedEnvironment,
};
state.manifest = manifest;
}
function removeClosedPreservedOwnedSessions(sandboxState, agentKeys) {
return removeClosedSerializedSessions(sandboxState, agentKeys, {
requirePreservedSession: true,
});
}
async function serializedSessionStateRequiresFreshCreationForOptions(client, state, options = {}) {
return ((await client.serializedSessionStateRequiresFreshCreationForOptions?.(state, options)) ?? false);
}
function serializedOwnedSessionReplacementError(client) {
return new UserError(`Sandbox client "${client.backendId}" cannot safely replace an owned sandbox from untrusted serialized state because remote ownership cannot be verified. Reconnect through a caller-managed sandbox selection, or explicitly clean up the old sandbox before starting a new run.`);
}
function lifecycleErrorForCloseErrors(message, errors) {
return errors.length === 1
? errors[0]
: new SandboxLifecycleError(message, { errors });
}
function removeClosedSerializedSessions(sandboxState, agentKeys, options = {}) {
for (const agentKey of agentKeys) {
delete sandboxState.sessionsByAgent[agentKey];
}
const remainingEntries = Object.values(sandboxState.sessionsByAgent);
if (options.requirePreservedSession &&
!remainingEntries.some((entry) => entry.preservedOwnedSession)) {
return false;
}
const currentEntry = sandboxState.sessionsByAgent[sandboxState.currentAgentKey];
if (currentEntry) {
sandboxState.currentAgentName = currentEntry.currentAgentName;
sandboxState.sessionState = currentEntry.sessionState;
return true;
}
const nextEntry = options.requirePreservedSession
? remainingEntries.find((entry) => entry.preservedOwnedSession)
: remainingEntries[0];
if (!nextEntry) {
return false;
}
sandboxState.currentAgentKey = nextEntry.currentAgentKey;
sandboxState.currentAgentName = nextEntry.currentAgentName;
sandboxState.sessionState = nextEntry.sessionState;
return true;
}
//# sourceMappingURL=manager.mjs.map