@openai/agents-core
Version:
The OpenAI Agents SDK is a lightweight yet powerful framework for building multi-agent workflows.
392 lines • 20.2 kB
JavaScript
import { UserError } from "../../errors.mjs";
import { SandboxMountError } from "../errors.mjs";
import { isEnvValueReference, isSerializedEnvValueReference, manifestAcknowledgesInContainerMountCredentialExposure, Manifest, replaceManifestMountCredentialExposurePolicy, } from "../manifest.mjs";
import { serializeManifestEnvironment } from "../shared/environment.mjs";
import { stableJsonStringify } from "../shared/stableJson.mjs";
import { SANDBOX_SESSION_STATE_VERSION, SUPPORTED_SANDBOX_SESSION_STATE_VERSIONS, } from "../session.mjs";
import { assertMountCredentialsRebound, assertHostPathGrantsRebound, deserializeManifest, deserializeMountCredentialRedactionMetadata, deserializeHostPathGrantRedactionMetadata, rebindPersistedPathGrants, rebindPersistedMountCredentials, serializeHostPathGrantRedactionMetadata, serializeMountCredentialRedactionMetadata, serializeManifest, serializeManifestRecord, } from "../sandboxes/shared/manifestPersistence.mjs";
import { manifestHasInContainerMounts, persistedManifestRecordHasNonResumableMountAuthority, NON_RESUMABLE_MOUNT_AUTHORITY_KEY, REDACTED_MOUNT_CREDENTIAL_PATHS_KEY, resolveAndValidateMountEnvironment, sanitizePersistedManifestRecord, sanitizeMountCredentialEnvironmentForPersistence, validateMountCredentialRedactionPaths, validateMountCredentialBoundaries, validateMountEnvironmentCredentialBoundaries, } from "../mountSecurity.mjs";
import { markRunStateSessionState, } from "../internal/sessionStateTrust.mjs";
export function getSerializedSandboxState(runState) {
return runState?._sandbox;
}
export function sanitizeSerializedSandboxState(state) {
const sessionsByAgent = Object.fromEntries(Object.entries(state.sessionsByAgent).map(([agentKey, entry]) => [
agentKey,
{
...entry,
sessionState: sanitizeSerializedSessionEnvelope(entry.sessionState),
},
]));
return {
...state,
sessionState: sanitizeSerializedSessionEnvelope(state.sessionState),
sessionsByAgent,
};
}
function sanitizeSerializedSessionEnvelope(envelope) {
const hasNonResumableMountAuthority = persistedManifestRecordHasNonResumableMountAuthority(envelope.manifest);
const existingMetadata = deserializeMountCredentialRedactionMetadata(envelope.providerState);
const existingPaths = existingMetadata[REDACTED_MOUNT_CREDENTIAL_PATHS_KEY] ?? [];
const { record, credentialPaths } = sanitizePersistedManifestRecord(envelope.manifest);
const persistedManifest = deserializeManifest(record);
const redactedPaths = [...new Set([...existingPaths, ...credentialPaths])];
validateMountCredentialRedactionPaths(persistedManifest, redactedPaths);
const serializedSanitizedManifest = serializeManifestRecord(sanitizeMountCredentialEnvironmentForPersistence({
manifest: persistedManifest,
}).manifest);
const persistentManifest = {
...record,
entries: serializedSanitizedManifest.entries,
environment: serializedSanitizedManifest.environment,
};
const providerState = providerStateWithoutSdkEnvelopeFields(envelope.providerState, envelope.backendId);
const persistedEnvironment = isStringRecord(providerState.environment)
? providerState.environment
: undefined;
delete providerState.environment;
if (persistedEnvironment) {
providerState.environment =
sanitizeMountCredentialEnvironmentForPersistence({
manifest: persistedManifest,
environment: persistedEnvironment,
}).environment;
}
if (redactedPaths.length > 0) {
providerState[REDACTED_MOUNT_CREDENTIAL_PATHS_KEY] = redactedPaths;
}
else {
delete providerState[REDACTED_MOUNT_CREDENTIAL_PATHS_KEY];
}
if (hasNonResumableMountAuthority) {
providerState[NON_RESUMABLE_MOUNT_AUTHORITY_KEY] = true;
}
return {
...envelope,
manifest: persistentManifest,
providerState,
};
}
export function toSessionStateEnvelope(backendId, state, providerState, options = {}) {
const stateForEnvelope = options.environmentPreparation === 'client' && options.trustedManifest
? {
...state,
manifest: mergeClientPreparedManifestAuthority(state.manifest, options.trustedManifest),
}
: state;
validateMountEnvironmentCredentialBoundaries(stateForEnvelope.manifest, options.environmentPreparation === 'client'
? {}
: (stateForEnvelope.environment ?? {}), options.environmentPreparation === 'client'
? { preserveDeferredValuePresence: true }
: {});
const persistentManifest = serializeManifest(sanitizeMountCredentialEnvironmentForPersistence(stateForEnvelope).manifest);
return {
version: SANDBOX_SESSION_STATE_VERSION,
backendId,
...(options.environmentPreparation
? { environmentPreparation: options.environmentPreparation }
: {}),
manifest: serializeManifestRecord(persistentManifest),
...(state.snapshot !== undefined ? { snapshot: state.snapshot } : {}),
...(state.snapshotFingerprint !== undefined
? { snapshotFingerprint: state.snapshotFingerprint }
: {}),
...(state.snapshotFingerprintVersion !== undefined
? { snapshotFingerprintVersion: state.snapshotFingerprintVersion }
: {}),
workspaceReady: state.workspaceReady ?? true,
...(state.exposedPorts
? { exposedPorts: structuredClone(state.exposedPorts) }
: {}),
// Keep provider-owned fields separate from the SDK envelope so version and backend
// checks can run before deserializing provider-specific state.
providerState: {
...providerStateWithoutSdkEnvelopeFields(providerState, backendId),
...serializeHostPathGrantRedactionMetadata(stateForEnvelope),
...serializeMountCredentialRedactionMetadata(stateForEnvelope),
},
};
}
export function mergeClientPreparedManifestAuthority(providerManifest, trustedManifest) {
const merged = new Manifest({
version: providerManifest.version,
root: providerManifest.root,
entries: structuredClone(providerManifest.entries),
environment: cloneManifestEnvironment(trustedManifest.environment),
users: structuredClone(providerManifest.users),
groups: structuredClone(providerManifest.groups),
extraPathGrants: structuredClone(trustedManifest.extraPathGrants),
remoteMountCommandAllowlist: [
...trustedManifest.remoteMountCommandAllowlist,
],
});
replaceManifestMountCredentialExposurePolicy(merged, trustedManifest);
return merged;
}
export function clientPreparedManifestAuthoritiesMatch(providerManifest, leftTrustedManifest, rightTrustedManifest) {
const left = mergeClientPreparedManifestAuthority(providerManifest, leftTrustedManifest);
const right = mergeClientPreparedManifestAuthority(providerManifest, rightTrustedManifest);
const environmentKeys = new Set([
...Object.keys(left.environment),
...Object.keys(right.environment),
]);
for (const key of environmentKeys) {
if (left.environment[key]?.resolver !== right.environment[key]?.resolver) {
return false;
}
}
const mountCredentialExposure = (manifest) => manifest.mountTargets().map(({ mountPath }) => ({
mountPath,
mountScoped: manifestAcknowledgesInContainerMountCredentialExposure(manifest, mountPath, 'mount_scoped'),
broad: manifestAcknowledgesInContainerMountCredentialExposure(manifest, mountPath, 'broad'),
}));
return (stableJsonStringify({
environment: serializeManifestEnvironment(left),
extraPathGrants: left.extraPathGrants,
remoteMountCommandAllowlist: left.remoteMountCommandAllowlist,
mountCredentialExposure: mountCredentialExposure(left),
}) ===
stableJsonStringify({
environment: serializeManifestEnvironment(right),
extraPathGrants: right.extraPathGrants,
remoteMountCommandAllowlist: right.remoteMountCommandAllowlist,
mountCredentialExposure: mountCredentialExposure(right),
}));
}
function providerStateWithoutSdkEnvelopeFields(providerState, backendId) {
const sanitized = { ...providerState };
delete sanitized.manifest;
if (backendId === 'docker') {
delete sanitized.sessionIdentity;
}
delete sanitized.snapshot;
delete sanitized.snapshotFingerprint;
delete sanitized.snapshotFingerprintVersion;
delete sanitized.workspaceReady;
delete sanitized.exposedPorts;
return sanitized;
}
export function assertSessionStateEnvelope(client, envelope) {
if (!SUPPORTED_SANDBOX_SESSION_STATE_VERSIONS.includes(envelope.version)) {
throw new UserError(`Sandbox session state version ${envelope.version} is not supported. Please use version ${SANDBOX_SESSION_STATE_VERSION}.`);
}
if (envelope.backendId !== client.backendId) {
throw new UserError('RunState sandbox session backend does not match the configured sandbox client.');
}
if (envelope.environmentPreparation !== undefined &&
envelope.environmentPreparation !== 'client') {
throw new UserError('RunState sandbox session environment preparation mode is not supported.');
}
const clientPreparesEnvironment = client.environmentPreparation === 'client';
const envelopeUsesClientPreparation = envelope.environmentPreparation === 'client';
if (clientPreparesEnvironment !== envelopeUsesClientPreparation) {
throw new UserError('RunState sandbox session environment preparation does not match the configured sandbox client.');
}
if (envelopeUsesClientPreparation &&
envelope.version !== SANDBOX_SESSION_STATE_VERSION) {
throw new UserError('Client-owned environment preparation requires the current sandbox session state version.');
}
}
export async function deserializeSandboxSessionStateEntry(client, serializedEntry, trustedManifest, trustedConfig = {}, options = {}) {
if (!serializedEntry) {
return undefined;
}
if (serializedEntry.backendId !== client.backendId) {
throw new UserError('RunState sandbox backend does not match the configured sandbox client.');
}
const resolvedTrustedManifest = options.trustedManifestAlreadyResolved
? trustedManifest
: resolveTrustedManifestForResume(client, trustedManifest, trustedConfig.clientOptions);
assertTrustedManifestForDockerRunState(client, resolvedTrustedManifest);
if (!client.deserializeSessionState) {
throw new UserError('Sandbox client must implement deserializeSessionState() to resume RunState sandbox state.');
}
const envelope = serializedEntry.sessionState;
assertSessionStateEnvelope(client, envelope);
const clientPreparesEnvironment = client.environmentPreparation === 'client';
if (clientPreparesEnvironment && !resolvedTrustedManifest) {
throw new UserError('Client-owned environment preparation requires a current trusted manifest before resuming sandbox session state.');
}
assertTrustedEnvironmentForPersistedReferences(envelope, resolvedTrustedManifest, { requireMatchingReferences: !clientPreparesEnvironment });
if (manifestHasInContainerMounts(deserializeManifest(envelope.manifest))) {
throw new SandboxMountError('Sandbox session state with in-container mounts cannot be resumed safely. Create a fresh sandbox so mount helpers are recreated from current trusted configuration.', undefined, 'mount_config_invalid');
}
const prevalidatedPersistedState = rebindPersistedMountCredentials({
manifest: deserializeManifest(envelope.manifest),
...deserializeMountCredentialRedactionMetadata(envelope.providerState),
}, resolvedTrustedManifest);
assertMountCredentialsRebound(prevalidatedPersistedState);
client.validateSessionStateForResume?.({
source: 'runState',
state: providerStateWithSdkEnvelopeFields(envelope, prevalidatedPersistedState.manifest, clientPreparesEnvironment ? undefined : resolvedTrustedManifest),
}, {
clientOptions: trustedConfig.clientOptions,
...(clientPreparesEnvironment
? { trustedManifest: resolvedTrustedManifest }
: {}),
});
const materializedTrustedManifest = resolvedTrustedManifest
? clientPreparesEnvironment
? resolvedTrustedManifest
: await resolveAndValidateMountEnvironment(resolvedTrustedManifest)
: undefined;
const state = await client.deserializeSessionState(providerStateWithSdkEnvelopeFields(envelope, prevalidatedPersistedState.manifest, clientPreparesEnvironment ? undefined : materializedTrustedManifest));
if (client.backendId === 'docker' && !clientPreparesEnvironment) {
delete state.sessionIdentity;
}
const hostPathMetadata = deserializeHostPathGrantRedactionMetadata(envelope.providerState);
const mountCredentialMetadata = deserializeMountCredentialRedactionMetadata(envelope.providerState);
const credentialReboundState = clientPreparesEnvironment
? {
...prevalidatedPersistedState,
...hostPathMetadata,
}
: rebindPersistedMountCredentials({
...state,
...hostPathMetadata,
...mountCredentialMetadata,
}, materializedTrustedManifest);
const reboundState = rebindPersistedPathGrants(credentialReboundState, materializedTrustedManifest, {
replaceWithTrustedManifest: client.backendId === 'docker' &&
materializedTrustedManifest !== undefined,
});
assertMountCredentialsRebound(reboundState);
validateMountCredentialBoundaries(reboundState.manifest);
validateMountEnvironmentCredentialBoundaries(clientPreparesEnvironment
? resolvedTrustedManifest
: reboundState.manifest, clientPreparesEnvironment ? {} : (reboundState.environment ?? {}), clientPreparesEnvironment ? { preserveDeferredValuePresence: true } : {});
assertHostPathGrantsRebound(reboundState);
if (clientPreparesEnvironment) {
return markRunStateSessionState(state, trustedConfig);
}
return markRunStateSessionState(reboundState, trustedConfig);
}
export function resolveTrustedManifestForResume(client, trustedManifest, clientOptions) {
if (!trustedManifest) {
return undefined;
}
const resolved = client.resolveTrustedManifestForResume
? client.resolveTrustedManifestForResume(trustedManifest, clientOptions)
: trustedManifest;
validateMountCredentialBoundaries(resolved);
return resolved;
}
export function assertTrustedManifestForDockerRunState(client, trustedManifest) {
if (client.backendId === 'docker' && trustedManifest === undefined) {
throw new UserError('Docker RunState resume requires a current trusted manifest for the sandbox agent.');
}
}
function providerStateWithSdkEnvelopeFields(envelope, persistedManifest, trustedManifest) {
const providerState = providerStateWithoutSdkEnvelopeFields(envelope.providerState, envelope.backendId);
const persistedEnvironment = isStringRecord(providerState.environment)
? providerState.environment
: undefined;
const sanitizedEnvironment = persistedEnvironment
? sanitizeMountCredentialEnvironmentForPersistence({
manifest: trustedManifest ?? persistedManifest,
environment: persistedEnvironment,
}).environment
: undefined;
return {
...providerState,
...(sanitizedEnvironment ? { environment: sanitizedEnvironment } : {}),
manifest: trustedManifest
? {
...serializeManifestRecord(persistedManifest),
environment: cloneManifestEnvironment(trustedManifest.environment),
}
: serializeManifestRecord(persistedManifest),
...(envelope.snapshot !== undefined ? { snapshot: envelope.snapshot } : {}),
...(envelope.snapshotFingerprint !== undefined
? { snapshotFingerprint: envelope.snapshotFingerprint }
: {}),
...(envelope.snapshotFingerprintVersion !== undefined
? {
snapshotFingerprintVersion: envelope.snapshotFingerprintVersion,
}
: {}),
workspaceReady: envelope.workspaceReady,
...(envelope.exposedPorts
? { exposedPorts: structuredClone(envelope.exposedPorts) }
: {}),
};
}
function isStringRecord(value) {
return (typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.values(value).every((entry) => typeof entry === 'string'));
}
function assertTrustedEnvironmentForPersistedReferences(envelope, trustedManifest, options = {}) {
const persistedEnvironment = envelope.manifest.environment;
if (!persistedEnvironment ||
typeof persistedEnvironment !== 'object' ||
Array.isArray(persistedEnvironment)) {
return;
}
const referenceKeys = Object.entries(persistedEnvironment)
.filter(([, value]) => isSerializedEnvValueReference(value))
.map(([key]) => key);
if (referenceKeys.length === 0) {
return;
}
if (!trustedManifest) {
throw new UserError('RunState sandbox session state with environment value references requires a current trusted manifest for the sandbox agent.');
}
if (options.requireMatchingReferences === false) {
return;
}
const missingReferenceKeys = referenceKeys.filter((key) => !trustedManifest.environment[key] ||
!isEnvValueReference(trustedManifest.environment[key]));
if (missingReferenceKeys.length > 0) {
throw new UserError(`RunState sandbox session state contains environment value references that are not present in the current trusted manifest: ${missingReferenceKeys.join(', ')}.`);
}
}
function cloneManifestEnvironment(environment) {
return Object.fromEntries(Object.entries(environment).map(([key, value]) => [key, value.init()]));
}
export function getSerializedSessionEntryForAgent(sandboxState, currentAgentKey) {
if (!sandboxState) {
return undefined;
}
return (sandboxState.sessionsByAgent?.[currentAgentKey] ??
(sandboxState.currentAgentKey === currentAgentKey
? {
backendId: sandboxState.backendId,
currentAgentKey: sandboxState.currentAgentKey,
currentAgentName: sandboxState.currentAgentName,
sessionState: sandboxState.sessionState,
}
: undefined));
}
export function serializedSessionEntryHasInContainerMounts(entry) {
return entry
? manifestHasInContainerMounts(deserializeManifest(entry.sessionState.manifest))
: false;
}
export function serializedSessionEntryRequiresFreshMountAuthority(entry) {
return (serializedSessionEntryHasInContainerMounts(entry) ||
entry?.sessionState.providerState[NON_RESUMABLE_MOUNT_AUTHORITY_KEY] ===
true);
}
export function getPreviousSerializedSessionsByAgent(sandboxState, client) {
if (!sandboxState || sandboxState.backendId !== client.backendId) {
return {};
}
const entries = {
...(sandboxState.sessionsByAgent ?? {}),
};
if (sandboxState.currentAgentKey && !entries[sandboxState.currentAgentKey]) {
entries[sandboxState.currentAgentKey] = {
backendId: sandboxState.backendId,
currentAgentKey: sandboxState.currentAgentKey,
currentAgentName: sandboxState.currentAgentName,
sessionState: sandboxState.sessionState,
};
}
return Object.fromEntries(Object.entries(entries).filter(([, entry]) => entry.backendId === client.backendId));
}
export function hasPreservedOwnedSessions(sandboxState) {
return Object.values(sandboxState?.sessionsByAgent ?? {}).some((entry) => entry.preservedOwnedSession);
}
//# sourceMappingURL=sessionState.mjs.map