From 377f90989deb7f7c5bfbd96297a64c8bfd333176 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 4 Dec 2025 14:18:32 +1000 Subject: [PATCH] fix: properly pass env vars to cached tools and add extensive logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEM: - Tool wrappers cached env vars in closure, so cached tools used stale env - Client env vars weren't reaching Railway executor even when provided - No visibility into env var flow through the system SOLUTION: 1. Store env vars per conversation in conversationEnv Map 2. Tool execute functions look up latest env from Map (not closure) 3. Chat API calls setConversationEnv() on each request 4. Added logging at every step 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/playground/src/app/api/chat/route.ts | 13 +++-- .../playground/src/lib/dynamic-tool-loader.ts | 47 ++++++++++++++++--- apps/railway-executor/server.ts | 15 ++++++ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/apps/playground/src/app/api/chat/route.ts b/apps/playground/src/app/api/chat/route.ts index 4ef7247..11c0ba2 100644 --- a/apps/playground/src/app/api/chat/route.ts +++ b/apps/playground/src/app/api/chat/route.ts @@ -3,7 +3,11 @@ import { searchTpmjsToolsTool } from '@tpmjs/search-registry'; import { type UIMessage, convertToModelMessages, stepCountIs, streamText } from 'ai'; import type { NextRequest } from 'next/server'; import { env } from '~/env'; -import { addConversationTools, loadToolsBatch } from '~/lib/dynamic-tool-loader'; +import { + addConversationTools, + loadToolsBatch, + setConversationEnv, +} from '~/lib/dynamic-tool-loader'; import { loadAllTools, sanitizeToolName } from '~/lib/tool-loader'; export const runtime = 'nodejs'; @@ -28,7 +32,10 @@ export async function POST(request: NextRequest) { const clientEnv: Record = body.env || {}; console.log(`🔑 Conversation ID: ${conversationId}`); - console.log(`🔐 Client env vars: ${Object.keys(clientEnv).length} keys`); + console.log(`🔐 Client env vars: ${Object.keys(clientEnv).length} keys`, Object.keys(clientEnv)); + + // Store env vars for this conversation (so cached tools can access them) + setConversationEnv(conversationId, clientEnv); // Initialize OpenAI with client-provided or server API key const apiKey = clientEnv.OPENAI_API_KEY || env.OPENAI_API_KEY; @@ -148,7 +155,7 @@ export async function POST(request: NextRequest) { })); try { - const loadedTools = await loadToolsBatch(toolsToLoad, clientEnv); + const loadedTools = await loadToolsBatch(toolsToLoad, conversationId, clientEnv); console.log(`✅ Successfully loaded ${Object.keys(loadedTools).length} tools`); // Add sanitized tools to conversation state diff --git a/apps/playground/src/lib/dynamic-tool-loader.ts b/apps/playground/src/lib/dynamic-tool-loader.ts index 438afd7..8acf005 100644 --- a/apps/playground/src/lib/dynamic-tool-loader.ts +++ b/apps/playground/src/lib/dynamic-tool-loader.ts @@ -6,6 +6,9 @@ const moduleCache = new Map(); // Cache for per-conversation active tools const conversationTools = new Map>(); +// Cache for per-conversation env vars (updated on each request) +const conversationEnv = new Map>(); + // Railway service URL const RAILWAY_SERVICE_URL = process.env.RAILWAY_SERVICE_URL || process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3001'; @@ -17,6 +20,22 @@ function getCacheKey(packageName: string, exportName: string): string { return `${packageName}::${exportName}`; } +/** + * Set environment variables for a conversation + * This allows tools to access the latest env vars even when cached + */ +export function setConversationEnv(conversationId: string, env: Record): void { + console.log(`🔑 Setting env for conversation ${conversationId}:`, Object.keys(env)); + conversationEnv.set(conversationId, env); +} + +/** + * Get environment variables for a conversation + */ +function getConversationEnv(conversationId: string): Record { + return conversationEnv.get(conversationId) || {}; +} + /** * Dynamically load a tool via Railway service * Railway service runs with --experimental-network-imports and can import from esm.sh @@ -26,6 +45,7 @@ export async function loadToolDynamically( packageName: string, exportName: string, version: string, + conversationId: string, importUrl?: string, env?: Record ): Promise { @@ -81,6 +101,10 @@ export async function loadToolDynamically( execute: async (params: any) => { console.log(`🚀 Executing ${packageName}/${exportName} remotely with params:`, params); + // Get the latest env vars for this conversation (not from closure!) + const currentEnv = getConversationEnv(conversationId); + console.log(`🔐 Using env vars for conversation ${conversationId}:`, Object.keys(currentEnv)); + const execResponse = await fetch(`${RAILWAY_SERVICE_URL}/execute-tool`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -90,7 +114,7 @@ export async function loadToolDynamically( version, importUrl: importUrl || `https://esm.sh/${packageName}@${version}`, params, - env: env || {}, + env: currentEnv, }), }); @@ -129,15 +153,24 @@ export async function loadToolsBatch( version: string; importUrl?: string; }>, + conversationId: string, env?: Record ): Promise> { + console.log(`📦 Loading ${toolMetadata.length} tools for conversation ${conversationId}`); + console.log(`🔑 Env vars being passed:`, Object.keys(env || {})); + const promises = toolMetadata.map((meta) => - loadToolDynamically(meta.packageName, meta.exportName, meta.version, meta.importUrl, env).then( - (tool) => ({ - key: getCacheKey(meta.packageName, meta.exportName), - tool, - }) - ) + loadToolDynamically( + meta.packageName, + meta.exportName, + meta.version, + conversationId, + meta.importUrl, + env + ).then((tool) => ({ + key: getCacheKey(meta.packageName, meta.exportName), + tool, + })) ); const results = await Promise.all(promises); diff --git a/apps/railway-executor/server.ts b/apps/railway-executor/server.ts index 117f0b0..c866293 100644 --- a/apps/railway-executor/server.ts +++ b/apps/railway-executor/server.ts @@ -173,6 +173,14 @@ async function executeTool(req: Request): Promise { const body = await req.json(); const { packageName, exportName, version, importUrl, params, env } = body; + console.log(`📥 Execute request:`, { + packageName, + exportName, + version, + envKeys: env ? Object.keys(env) : [], + envValues: env || {}, + }); + if (!packageName || !exportName || !version) { return Response.json( { @@ -220,8 +228,15 @@ async function executeTool(req: Request): Promise { console.log(`🔐 Injecting ${envKeys.length} environment variables:`, envKeys); for (const [key, value] of Object.entries(env)) { Deno.env.set(key, String(value)); + console.log(` ✅ Set ${key} = ${String(value).substring(0, 10)}...`); } + // Verify they're set + console.log(`🔍 Verification - Deno.env has:`, envKeys.map(k => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`)); + } else { + console.log(`⚠️ No env vars provided in request`); } + } else { + console.log(`⚠️ No env object in request body`); } // Execute the tool