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
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "5.1.6",
"@librechat/agents": "^3.8.7",
"@librechat/agents": "^3.8.8",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down
15 changes: 15 additions & 0 deletions api/server/services/Files/Code/process.js
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,7 @@ async function readSandboxFile({
* @param {Object} params
* @param {string} params.file_path
* @param {string} params.workspace_id
* @param {string} [params.workspace_instance_id]
* @param {number} params.start_line
* @param {number} params.max_lines
* @param {string} params.codeApiBaseUrl
Expand All @@ -1123,6 +1124,7 @@ async function readSandboxFile({
async function readWorkspaceFile({
file_path,
workspace_id,
workspace_instance_id,
start_line,
max_lines,
codeApiBaseUrl,
Expand All @@ -1144,6 +1146,7 @@ async function readWorkspaceFile({
protocolVersion: 1,
operation: 'read_file',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
path: file_path,
startLine: start_line,
maxLines: max_lines,
Expand All @@ -1158,6 +1161,7 @@ async function readWorkspaceFile({
* @param {Object} params
* @param {string} params.query
* @param {string} params.workspace_id
* @param {string} [params.workspace_instance_id]
* @param {string} [params.path]
* @param {number} params.max_results
* @param {string} params.codeApiBaseUrl
Expand All @@ -1169,6 +1173,7 @@ async function readWorkspaceFile({
async function searchWorkspace({
query,
workspace_id,
workspace_instance_id,
path,
max_results,
codeApiBaseUrl,
Expand All @@ -1190,6 +1195,7 @@ async function searchWorkspace({
protocolVersion: 1,
operation: 'search_text',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
query,
...(path ? { path } : {}),
maxResults: max_results,
Expand All @@ -1203,6 +1209,7 @@ async function searchWorkspace({
*
* @param {Object} params
* @param {string} params.workspace_id
* @param {string} [params.workspace_instance_id]
* @param {string} [params.path]
* @param {string} [params.after_path]
* @param {number} params.max_results
Expand All @@ -1214,6 +1221,7 @@ async function searchWorkspace({
*/
async function listWorkspaceFiles({
workspace_id,
workspace_instance_id,
path,
after_path,
max_results,
Expand All @@ -1236,6 +1244,7 @@ async function listWorkspaceFiles({
protocolVersion: 1,
operation: 'list_files',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
...(path ? { path } : {}),
...(after_path ? { afterPath: after_path } : {}),
maxResults: max_results,
Expand All @@ -1250,6 +1259,7 @@ async function writeWorkspaceFile({
content,
overwrite,
workspace_id,
workspace_instance_id,
codeApiBaseUrl,
executionProfile,
bridgeWorkerId,
Expand All @@ -1269,6 +1279,7 @@ async function writeWorkspaceFile({
protocolVersion: 1,
operation: 'write_file',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
path: file_path,
content,
overwrite,
Expand All @@ -1283,6 +1294,7 @@ async function editWorkspaceFile({
edits,
expected_base_sha256,
workspace_id,
workspace_instance_id,
codeApiBaseUrl,
executionProfile,
bridgeWorkerId,
Expand All @@ -1302,6 +1314,7 @@ async function editWorkspaceFile({
protocolVersion: 1,
operation: 'edit_file',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
path: file_path,
edits,
...(expected_base_sha256 ? { expectedBaseSha256: expected_base_sha256 } : {}),
Expand All @@ -1315,6 +1328,7 @@ async function previewWorkspaceEdit({
file_path,
edits,
workspace_id,
workspace_instance_id,
codeApiBaseUrl,
executionProfile,
bridgeWorkerId,
Expand All @@ -1334,6 +1348,7 @@ async function previewWorkspaceEdit({
protocolVersion: 1,
operation: 'preview_edit',
workspaceId: workspace_id,
...(workspace_instance_id ? { workspaceInstanceId: workspace_instance_id } : {}),
path: file_path,
edits,
},
Expand Down
4 changes: 4 additions & 0 deletions api/server/services/Files/Code/process.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,7 @@ describe('Code Process', () => {
readWorkspaceFile({
file_path: 'src/app.ts',
workspace_id: 'primary',
workspace_instance_id: 'a'.repeat(64),
start_line: 1,
max_lines: 200,
codeApiBaseUrl: 'https://attached-code.example.com/v1',
Expand Down Expand Up @@ -2082,6 +2083,7 @@ describe('Code Process', () => {
protocolVersion: 1,
operation: 'read_file',
workspaceId: 'primary',
workspaceInstanceId: 'a'.repeat(64),
path: 'src/app.ts',
startLine: 1,
maxLines: 200,
Expand Down Expand Up @@ -2227,6 +2229,7 @@ describe('Code Process', () => {
content: 'ready',
overwrite: false,
workspace_id: 'primary',
workspace_instance_id: 'b'.repeat(64),
codeApiBaseUrl: 'https://attached-code.example.com/v1',
executionProfile: 'stateful',
bridgeWorkerId: 'worker-user-1',
Expand Down Expand Up @@ -2257,6 +2260,7 @@ describe('Code Process', () => {
protocolVersion: 1,
operation: 'write_file',
workspaceId: 'primary',
workspaceInstanceId: 'b'.repeat(64),
path: 'src/new.ts',
content: 'ready',
overwrite: false,
Expand Down
1 change: 1 addition & 0 deletions api/server/services/ToolService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,7 @@ async function loadToolsForExecution({
authHeaders,
baseUrl: codeExecutionContext.baseUrl,
workspaceId: codeExecutionContext.codeWorkspace.workspaceId,
workspaceInstanceId: codeExecutionContext.codeWorkspace.workspaceInstanceId,
environment: codeExecutionContext.codeWorkspace.environment,
gitIdentity: agent?.git_identity,
maxTimeoutMs: resolveAttachedWorkspaceCommandTimeoutMax(
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "5.1.6",
"@librechat/agents": "^3.8.7",
"@librechat/agents": "^3.8.8",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.30.0",
"@opentelemetry/api": "^1.9.0",
Expand Down
45 changes: 45 additions & 0 deletions packages/api/src/agents/execution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ describe('resolveCodeExecutionContext', () => {
expect(context.executionRouteKey).toMatch(/^stateful:[a-f0-9]{32}$/);
});

it('derives a stable opaque worktree identity from authenticated conversation scope', () => {
const resolve = (conversationId: string, userId = 'user-1') =>
resolveCodeExecutionContext({
statefulSessions: true,
environmentId: 'my-vm',
environments: [
{
id: 'my-vm',
name: 'My VM',
type: 'attached',
baseURL: 'https://bridge.example/v1',
workerId: 'worker',
owner: 'deployment',
},
],
userId,
conversationId,
}).conversationWorkspaceInstanceId;

const first = resolve('conversation-1');
expect(first).toMatch(/^[a-f0-9]{64}$/);
expect(resolve('conversation-1')).toBe(first);
expect(resolve('conversation-2')).not.toBe(first);
expect(resolve('conversation-1', 'user-2')).not.toBe(first);
expect(first).not.toContain('conversation-1');
});

it('routes a deployment worker declared in pairing metadata', () => {
const context = resolveCodeExecutionContext({
statefulSessions: true,
Expand Down Expand Up @@ -408,6 +435,24 @@ describe('stateful code approval target binding', () => {
).toThrow('changed while this action awaited approval');
});

it('requires new approval when the conversation worktree changes', () => {
const original = context();
original.codeWorkspace!.workspaceInstanceId = 'a'.repeat(64);
const expected = captureCodeExecutionApprovalBinding([
{ id: 'a', codeExecutionContext: original },
]);
const updated = context({
codeWorkspace: {
...original.codeWorkspace!,
workspaceInstanceId: 'b'.repeat(64),
},
});

expect(() =>
assertCodeExecutionApprovalBinding(expected, [{ id: 'a', codeExecutionContext: updated }]),
).toThrow('changed while this action awaited approval');
});

it('captures only opaque, canonical identities for stateful targets', () => {
const binding = captureCodeExecutionApprovalBinding([
{
Expand Down
12 changes: 12 additions & 0 deletions packages/api/src/agents/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ export interface CodeExecutionContext {
bridgeWorkerId?: string;
codeEnvironmentConfigSchema?: CodeEnvironmentUserConfigSchema;
codeEnvironmentSettings?: CodeEnvironmentUserSettings;
/** Server-derived conversation identity, activated only after worker capability negotiation. */
conversationWorkspaceInstanceId?: string;
/** Live, server-validated directory selection. Never derive session reuse from this field. */
codeWorkspace?: CodeWorkspaceSelection & {
operations: CodeWorkspaceOperation[];
workspaceInstanceId?: string;
/** Live Code API execution ceiling. Omitted by older deployments. */
maxCommandTimeoutMs?: number;
instructions?: CodeWorkspaceDescriptor['instructions'];
Expand Down Expand Up @@ -115,6 +118,7 @@ export function captureCodeExecutionApprovalBinding(
: {
environmentId: context.codeWorkspace.environmentId,
workspaceId: context.codeWorkspace.workspaceId,
workspaceInstanceId: context.codeWorkspace.workspaceInstanceId ?? null,
operations: [...new Set(context.codeWorkspace.operations)].sort(),
...(context.codeWorkspace.environment
? { definitionFingerprint: context.codeWorkspace.environment.fingerprint }
Expand Down Expand Up @@ -349,6 +353,13 @@ export function resolveCodeExecutionContext(params: {
conversationId: params.conversationId,
});
const executionRouteKey = createCodeExecutionRouteKey('stateful', configuredEnvironment);
const conversationWorkspaceInstanceId =
configuredEnvironment?.type === 'attached' && params.conversationId
? createHash('sha256')
.update('librechat-conversation-workspace-v1\0')
.update(JSON.stringify([params.userId, params.conversationId, configuredEnvironment.id]))
.digest('hex')
: undefined;
return {
baseUrl: getCodeExecutionBaseUrl('stateful', configuredEnvironment),
codeSessionKey: `${Constants.EXECUTE_CODE}:${executionRouteKey}:${runtimeSessionHint}`,
Expand All @@ -361,6 +372,7 @@ export function resolveCodeExecutionContext(params: {
bridgeWorkerId: configuredEnvironment?.workerId ?? configuredEnvironment?.pairing?.workerId,
codeEnvironmentConfigSchema: configuredEnvironment?.configSchema,
codeEnvironmentSettings: configuredEnvironment?.settings,
...(conversationWorkspaceInstanceId ? { conversationWorkspaceInstanceId } : {}),
};
}

Expand Down
16 changes: 16 additions & 0 deletions packages/api/src/agents/handlers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4582,6 +4582,13 @@ describe('createToolExecuteHandler', () => {
executionProfile: 'stateful',
statefulSessions: true,
environmentType: 'attached',
environmentId: 'personal-machine',
codeWorkspace: {
environmentId: 'personal-machine',
workspaceId: 'project-a',
workspaceInstanceId: 'a'.repeat(64),
operations: TEST_ATTACHED_WORKSPACE_OPERATIONS,
},
codeEnvironmentConfigSchema: { limits: { maxQueueWaitMs: 0 } },
bridgeWorkerId: 'user-worker',
},
Expand Down Expand Up @@ -4612,6 +4619,7 @@ describe('createToolExecuteHandler', () => {
content: 'export const ok = 1;',
overwrite: false,
workspace_id: 'project-a',
workspace_instance_id: 'a'.repeat(64),
maxQueueWaitMs: 0,
codeApiBaseUrl: 'https://code.example.com',
executionProfile: 'stateful',
Expand Down Expand Up @@ -5590,6 +5598,13 @@ describe('createToolExecuteHandler', () => {
codeSessionKey: 'execute_code:stateful:attached',
executionProfile: 'stateful',
environmentType: 'attached',
environmentId: 'personal-machine',
codeWorkspace: {
environmentId: 'personal-machine',
workspaceId: 'project-a',
workspaceInstanceId: 'b'.repeat(64),
operations: TEST_ATTACHED_WORKSPACE_OPERATIONS,
},
codeEnvironmentConfigSchema: { limits: { maxQueueWaitMs: 0 } },
bridgeWorkerId: 'personal-worker-1',
statefulSessions: true,
Expand All @@ -5609,6 +5624,7 @@ describe('createToolExecuteHandler', () => {
expect(readWorkspaceFile).toHaveBeenCalledWith({
file_path: 'src/app.ts',
workspace_id: 'project-a',
workspace_instance_id: 'b'.repeat(64),
maxQueueWaitMs: 0,
start_line: 1,
max_lines: 200,
Expand Down
Loading
Loading