Skip to content
Open
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
59 changes: 28 additions & 31 deletions packages/trueforge/src/apis/turns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ export type BeginTurnExecutionDeps = Pick<TurnsRouterDeps, 'activeTurns' | 'even
skillStore: Pick<ISkillStore, 'resolveTurnSkills'>;
};

/** Extra LLM/MCP headers resolved after turn id is minted. */
type ResolveTurnHeaders = (input: { session: SessionHandle; turnId: string }) => Record<string, string>;

interface BeginTurnExecutionParams {
session: SessionHandle;
input: TurnInputItem[] | undefined;
previous_turn_id: string | undefined;
userRef: string;
turnHeaders?: ResolveTurnHeaders;
deps: BeginTurnExecutionDeps;
}

/**
* Builds the per-turn resolver. Agent / MCP / sandbox / LLM lookups are wired
* the same way: async factories over the corresponding stores.
Expand All @@ -149,8 +161,7 @@ function createTurnResolver(deps: {
signal: AbortSignal;
userRef: string;
session: SessionHandle;
turnId: string;
tfyMetadata: Record<string, string> | undefined;
turnHeaders: Record<string, string>;
}): TurnResourceResolver {
const {
mcpServerStore,
Expand All @@ -162,14 +173,10 @@ function createTurnResolver(deps: {
signal,
userRef,
session,
turnId,
tfyMetadata,
turnHeaders,
} = deps;
const tenant_id = session.tenant_id;
const sessionId = session.session_id;
const metadataHeaders = isTrueFoundryModeEnabled()
? gatewayMetadataHeaders(mergeGatewayMetadata({ session, turnId, tfyMetadata }))
: {};

return new TurnResourceResolver({
llm: async name => {
Expand All @@ -182,7 +189,7 @@ function createTurnResolver(deps: {
modelClient: new VercelAILLM({
providerConfig: {
...resolved.providerConfig,
headers: { ...resolved.providerConfig.headers, ...metadataHeaders },
headers: { ...resolved.providerConfig.headers, ...turnHeaders },
},
logger,
signal,
Expand All @@ -207,7 +214,7 @@ function createTurnResolver(deps: {
url: connection.url,
headers: withGatewayMetadataHeaders({
headers: connection.headers,
metadataHeaders,
metadataHeaders: turnHeaders,
}),
};
},
Expand Down Expand Up @@ -386,15 +393,10 @@ export interface TurnEventDrainInput {
* Shared create-turn engine: persist the turn, start execution, and return the
* drain inputs. Does not wait for events and does not write HTTP/SSE.
*/
export async function beginTurnExecution(params: {
session: SessionHandle;
input: TurnInputItem[] | undefined;
previous_turn_id: string | undefined;
userRef: string;
tfyMetadata?: Record<string, string> | undefined;
deps: BeginTurnExecutionDeps;
}): Promise<{ turn: TurnHandle; drainInput: TurnEventDrainInput }> {
const { session, input, previous_turn_id: previousTurnId, userRef, tfyMetadata, deps } = params;
export async function beginTurnExecution(
params: BeginTurnExecutionParams,
): Promise<{ turn: TurnHandle; drainInput: TurnEventDrainInput }> {
const { session, input, previous_turn_id: previousTurnId, userRef, turnHeaders, deps } = params;
const sessionId = session.session_id;
const turnId = mintPeeredTurnId(configuration.EXECUTOR_ID);

Expand All @@ -410,8 +412,7 @@ export async function beginTurnExecution(params: {
signal: abortController.signal,
userRef,
session,
turnId,
tfyMetadata,
turnHeaders: turnHeaders?.({ session, turnId }) ?? {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scheduled turns drop gateway metadata

Medium Severity

TrueFoundry-mode x-tfy-metadata harness stamps now apply only on HTTP create-turn. startTurnInProcess callers such as schedule runs omit turnHeaders, so model and MCP gateway calls lose tfg.session_id, tfg.turn_id, and agent identity that were previously always overlaid.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Session metadata is public; omit leaves, {} clears

Reviewed by Cursor Bugbot for commit 4e5644c. Configure here.

});

// First turn only: derive the title from the first user message. The store
Expand Down Expand Up @@ -461,14 +462,7 @@ export async function beginTurnExecution(params: {
* Non-stream create-turn: begin execution and resolve once the first event is
* dual-written so immediate subscribe cannot 412. Same as `stream: false`.
*/
export async function startTurnInProcess(params: {
session: SessionHandle;
input: TurnInputItem[] | undefined;
previous_turn_id: string | undefined;
userRef: string;
tfyMetadata?: Record<string, string> | undefined;
deps: BeginTurnExecutionDeps;
}): Promise<TurnHandle> {
export async function startTurnInProcess(params: BeginTurnExecutionParams): Promise<TurnHandle> {
const { turn, drainInput } = await beginTurnExecution(params);

// Same unawaited drain scheduling as Hono streamSSE's run(cb).
Expand Down Expand Up @@ -775,14 +769,17 @@ export function createTurnsRouter(deps: TurnsRouterDeps) {
}

const rawTfyMetadata = c.req.header(X_TFY_METADATA);
const tfyMetadata = rawTfyMetadata === undefined ? undefined : parseGatewayMetadataHeader(rawTfyMetadata);
const requestMetadata = rawTfyMetadata === undefined ? undefined : parseGatewayMetadataHeader(rawTfyMetadata);

const turnParams = {
const turnParams: BeginTurnExecutionParams = {
session,
input: body.input,
previous_turn_id: body.previous_turn_id,
userRef: requestContext.subject.id,
tfyMetadata,
turnHeaders: ({ session: turnSession, turnId }) =>
isTrueFoundryModeEnabled()
? gatewayMetadataHeaders(mergeGatewayMetadata({ session: turnSession, turnId, requestMetadata }))
: {},
deps: {
...deps,
modelProviderStore: deps.resolveModelProviderStore(c, referencedAgent),
Expand Down
6 changes: 3 additions & 3 deletions packages/trueforge/src/runtime/sessionResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ export function buildGatewayMetadata(input: { session: SessionHandle; turnId: st
return metadata;
}

/** Inbound x-tfy-metadata first; harness tfg.* always win */
/** Caller requestMetadata first; harness tfg.* always win */
export function mergeGatewayMetadata(input: {
session: SessionHandle;
turnId: string;
tfyMetadata?: Record<string, string> | undefined;
requestMetadata?: Record<string, string> | undefined;
}): Record<string, string> {
return {
...input.tfyMetadata,
...input.requestMetadata,
...buildGatewayMetadata({ session: input.session, turnId: input.turnId }),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('buildGatewayMetadata', () => {
});

describe('mergeGatewayMetadata', () => {
it('keeps tfyMetadata keys and overwrites spoofed tfg.* fields so order is maintained', async () => {
it('keeps requestMetadata keys and overwrites spoofed tfg.* fields so order is maintained', async () => {
const session = await createGatewayMetadataSession({
agent: { type: 'reference', id: 'agent-1', name: 'my-agent' },
});
Expand All @@ -89,7 +89,7 @@ describe('mergeGatewayMetadata', () => {
mergeGatewayMetadata({
session,
turnId: 'turn-1',
tfyMetadata: {
requestMetadata: {
env: 'prod',
[`${TFG_METADATA_PREFIX}.session_id`]: 'spoofed-session',
[`${TFG_METADATA_PREFIX}.turn_id`]: 'spoofed-turn',
Expand All @@ -106,7 +106,7 @@ describe('mergeGatewayMetadata', () => {
});
});

it('matches harness-only stamps when tfyMetadata is absent', async () => {
it('matches harness-only stamps when requestMetadata is absent', async () => {
const session = await createGatewayMetadataSession({
agent: { type: 'reference', id: 'agent-1', name: 'my-agent' },
});
Expand Down
Loading