From 7d9321d9c6560f6cebd93a9e65f1b7001bad57cb Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Fri, 16 Jan 2026 14:59:00 +1000 Subject: [PATCH] feat: allow public access to agents and collections with caller credentials Users can now access other users' PUBLIC agents and collections by providing their own credentials in the request: For agents: - Provide `providerApiKey` for LLM access - Provide `env` object with tool environment variables - Owner's stored credentials are never shared For collections (MCP): - Provide `env` in params for tool environment variables - Owner's stored credentials are never shared Returns clear errors listing missing required env vars if not provided. Files changed: - packages/types/src/agent.ts: Add providerApiKey to SendMessageSchema - apps/web/src/lib/agents/env-helpers.ts: New helper functions for env vars - apps/web/src/lib/agents/build-tools.ts: Accept callerEnvVars parameter - apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts: Allow public agent access with caller credentials - apps/web/src/lib/mcp/handlers.ts: Accept callerEnvVars, validate requirements - apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts: Allow public collection access, pass isOwner flag - apps/web/src/app/docs/platform-guide/page.tsx: Update access model docs --- .../conversation/[conversationId]/route.ts | 109 +++-- .../[username]/[slug]/[transport]/route.ts | 54 ++- apps/web/src/app/docs/platform-guide/page.tsx | 374 ++++++++---------- apps/web/src/lib/agents/build-tools.ts | 16 +- apps/web/src/lib/agents/env-helpers.ts | 156 ++++++++ apps/web/src/lib/mcp/handlers.ts | 42 +- packages/types/src/agent.ts | 2 + 7 files changed, 478 insertions(+), 275 deletions(-) create mode 100644 apps/web/src/lib/agents/env-helpers.ts diff --git a/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts index 1712a64..bf4d58f 100644 --- a/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts +++ b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts @@ -117,23 +117,22 @@ export async function POST(request: NextRequest, context: RouteContext): Promise // Fetch agent with all tool relations using agent ID const { fetchAgentWithTools, buildAgentTools } = await import('@/lib/agents/build-tools'); + const { getRequiredEnvVarsForAgent, getMissingEnvVars } = await import( + '@/lib/agents/env-helpers' + ); const agent = await fetchAgentWithTools(agentId); if (!agent) { return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); } - // Owner-only enforcement: Only the agent owner can chat with the agent - if (authResult.userId !== agent.userId) { - return NextResponse.json( - { - success: false, - error: - 'Fork this agent to use it. Only the agent owner can chat with agents. ' + - 'Visit the agent page to fork it to your account.', - }, - { status: 403 } - ); + // Check ownership + const isOwner = authResult.userId === agent.userId; + + // Non-owners can only access PUBLIC agents + if (!isOwner && !agent.isPublic) { + // Don't reveal that the agent exists - return 404 + return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); } // Map provider to expected key name format @@ -153,29 +152,76 @@ export async function POST(request: NextRequest, context: RouteContext): Promise ); } - // Get user's API key for this provider - const userApiKey = await prisma.userApiKey.findUnique({ - where: { - userId_keyName: { - userId: agent.userId, - keyName, - }, - }, - }); + let apiKey: string; + let callerEnvVars: Record | undefined; - if (!userApiKey) { - return NextResponse.json( - { - success: false, - error: `No API key configured for ${agent.provider}. Please add your API key in settings.`, + if (isOwner) { + // Owner: use stored encrypted keys + const userApiKey = await prisma.userApiKey.findUnique({ + where: { + userId_keyName: { + userId: agent.userId, + keyName, + }, }, - { status: 400 } - ); + }); + + if (!userApiKey) { + return NextResponse.json( + { + success: false, + error: `No API key configured for ${agent.provider}. Please add your API key in settings.`, + }, + { status: 400 } + ); + } + + // Decrypt the API key + apiKey = decryptApiKey(userApiKey.encryptedKey, userApiKey.keyIv); + // callerEnvVars stays undefined - buildAgentTools will use agent's stored env vars + } else { + // Non-owner accessing public agent: must provide their own credentials + if (!parsed.data.providerApiKey) { + return NextResponse.json( + { + success: false, + error: 'Public agent access requires your own API key', + details: { + code: 'MISSING_PROVIDER_KEY', + requiredProvider: agent.provider, + hint: `Provide your ${agent.provider} API key in the 'providerApiKey' field`, + }, + }, + { status: 400 } + ); + } + + apiKey = parsed.data.providerApiKey; + callerEnvVars = parsed.data.env || {}; + + // Check for required environment variables + const requiredEnvVars = getRequiredEnvVarsForAgent(agent); + const missingEnvVars = getMissingEnvVars(requiredEnvVars, callerEnvVars); + + if (missingEnvVars.length > 0) { + return NextResponse.json( + { + success: false, + error: 'Missing required environment variables', + details: { + code: 'MISSING_ENV_VARS', + missingVars: missingEnvVars.map((e) => ({ + name: e.name, + description: e.description, + })), + hint: "Provide these variables in the 'env' field of your request", + }, + }, + { status: 400 } + ); + } } - // Decrypt the API key - const apiKey = decryptApiKey(userApiKey.encryptedKey, userApiKey.keyIv); - // Get or create conversation let conversation = await prisma.conversation.findUnique({ where: { @@ -285,7 +331,8 @@ export async function POST(request: NextRequest, context: RouteContext): Promise messages.push({ role: 'user', content: parsed.data.message }); // Build tools from agent configuration - const tools = buildAgentTools(agent); + // Build tools - pass callerEnvVars if non-owner accessing public agent + const tools = buildAgentTools(agent, callerEnvVars); // Get the provider model const model = await getProviderModel(agent.provider, agent.modelId, apiKey); diff --git a/apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts b/apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts index 2a63f49..e943054 100644 --- a/apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts +++ b/apps/web/src/app/api/mcp/[username]/[slug]/[transport]/route.ts @@ -87,7 +87,8 @@ interface JsonRpcResponse { async function processJsonRpcRequest( collectionId: string, collectionName: string, - body: JsonRpcRequest + body: JsonRpcRequest, + isOwner: boolean ): Promise { const requestId = body.id ?? null; @@ -98,12 +99,19 @@ async function processJsonRpcRequest( case 'tools/list': return await handleToolsList(collectionId, requestId); - case 'tools/call': - return await handleToolsCall( - collectionId, - body.params as { name: string; arguments?: Record }, - requestId - ); + case 'tools/call': { + const params = body.params as { + name: string; + arguments?: Record; + env?: Record; + }; + + // For non-owners, use caller-provided env vars (or empty if not provided) + // For owners, callerEnvVars is undefined so handleToolsCall uses stored env vars + const callerEnvVars = isOwner ? undefined : params.env || {}; + + return await handleToolsCall(collectionId, params, requestId, callerEnvVars); + } case 'notifications/initialized': case 'ping': @@ -125,7 +133,8 @@ async function processJsonRpcRequest( async function handleHttpTransport( request: NextRequest, collectionId: string, - collectionName: string + collectionName: string, + isOwner: boolean ): Promise { let body: JsonRpcRequest; try { @@ -137,7 +146,7 @@ async function handleHttpTransport( ); } - const response = await processJsonRpcRequest(collectionId, collectionName, body); + const response = await processJsonRpcRequest(collectionId, collectionName, body, isOwner); return NextResponse.json(response); } @@ -148,7 +157,8 @@ async function handleHttpTransport( async function handleSseTransport( request: NextRequest, collectionId: string, - collectionName: string + collectionName: string, + isOwner: boolean ): Promise { let body: JsonRpcRequest; try { @@ -167,7 +177,7 @@ async function handleSseTransport( ); } - const response = await processJsonRpcRequest(collectionId, collectionName, body); + const response = await processJsonRpcRequest(collectionId, collectionName, body, isOwner); // For SSE, we send the response as an event and then close const encoder = new TextEncoder(); @@ -331,7 +341,7 @@ export async function POST(request: NextRequest, context: RouteContext): Promise // Authorization check: // - Owners can always access their own collections (public or private) - // - Non-owners can only access public collections (and must fork to use) + // - Non-owners can access PUBLIC collections with their own env vars const isOwner = authResult.userId === collection.userId; if (!isOwner) { @@ -342,27 +352,15 @@ export async function POST(request: NextRequest, context: RouteContext): Promise { status: 404 } ); } - // Public collection but not the owner - they need to fork it - return NextResponse.json( - { - jsonrpc: '2.0', - error: { - code: -32403, - message: - 'Fork this collection to use it. Only the collection owner can execute tools via MCP. ' + - 'Visit the collection page to fork it to your account.', - }, - id: null, - }, - { status: 403 } - ); + // Public collection - non-owners can access but must provide their own env vars + // The env vars are validated per-tool in handleToolsCall } let response: Response; if (transport === 'sse') { - response = await handleSseTransport(request, collection.id, collection.name); + response = await handleSseTransport(request, collection.id, collection.name, isOwner); } else { - response = await handleHttpTransport(request, collection.id, collection.name); + response = await handleHttpTransport(request, collection.id, collection.name, isOwner); } // Track usage for authenticated requests diff --git a/apps/web/src/app/docs/platform-guide/page.tsx b/apps/web/src/app/docs/platform-guide/page.tsx index 3cf5587..f80d490 100644 --- a/apps/web/src/app/docs/platform-guide/page.tsx +++ b/apps/web/src/app/docs/platform-guide/page.tsx @@ -63,6 +63,15 @@ const NAV_SECTIONS = [ { id: 'api-usage', label: 'Usage Tracking' }, ], }, + { + title: 'Access Model', + items: [ + { id: 'access-model', label: 'Overview' }, + { id: 'agent-access', label: 'Agent API Access' }, + { id: 'collection-access', label: 'Collection MCP Access' }, + { id: 'error-reference', label: 'Error Reference' }, + ], + }, { title: 'Reference', items: [ @@ -1633,36 +1642,48 @@ X-RateLimit-Reset: 1704067200`} {/* ==================== ACCESS MODEL ==================== */} - +

- TPMJS uses a "fork to use"{' '} - model. You cannot directly use someone else's public agent or collection with - your API key—you must fork it first. + You can access public agents and collections using your own API key—but you must + provide all required credentials{' '} + (LLM keys, tool environment variables) in the request. The owner's stored + credentials are never shared.

-
-

- Important: Even with a valid API key, - you cannot call another user's agent or collection endpoints. You'll - receive a 403 error instructing you to fork the resource first. -

+
+
+

Owners

+
    +
  • • Use stored credentials from dashboard
  • +
  • • Access public and private resources
  • +
  • • No extra parameters needed in API calls
  • +
+
+
+

Non-Owners

+
    +
  • • Can only access PUBLIC resources
  • +
  • • Must provide credentials in each request
  • +
  • • Owner's credentials are never used
  • +
+
- +
  • - Security - Owners control their own - API keys and environment variables + Security - Owner credentials stay + private, never shared with callers
  • - Cost Control - You pay for your own - usage, not someone else's + Transparency - Callers pay for + their own LLM/tool usage
  • - Customization - Forking lets you - modify tools, prompts, and settings + Flexibility - Use public resources + without forking
  • - Privacy - Your conversations and - usage stay in your account + Control - Fork when you want to + customize
@@ -1670,239 +1691,164 @@ X-RateLimit-Reset: 1704067200`}

- When you call the agent conversation API, strict ownership checks apply. + Public agents can be accessed by any authenticated user who provides their own + credentials.

- +

- No. Even if an agent is public, you - must be the owner to use the conversation API. Attempting to call another - user's agent returns: + As the owner, just send your message—your stored LLM key and env vars are used:

- +

- The agent owner always pays. The - system uses the owner's stored LLM provider API keys (OpenAI, Anthropic, - etc.), not the caller's. + You must provide your own LLM provider key and any tool environment variables:

-
-

- Since you must own the agent to use it, and the owner's API keys are - used—you're always paying for your own usage. -

-
+
- +

- If you haven't configured your LLM provider API key, you'll receive: + If you don't provide providerApiKey for a public agent:

-

- Add your provider keys at{' '} - - Dashboard → Settings → API Keys - - . -

- -
-
-
- - 1 - - Find a public agent -
-

- Browse at tpmjs.com/{'{username}'}/agents/{'{uid}'} -

-
-
-
- - 2 - - Fork it to your account -
-

- Click "Fork" to create your own copy -

-
-
-
- - 3 - - Add your LLM API key -
-

- Configure your provider key (not copied during fork) -

-
-
-
- - 4 - - - Use your fork via API - -
-

- Call /api/{'{your-username}'}/agents/{'{uid}'}/conversation/{'{id}'} -

-
+ +

+ If the agent's tools require env vars you didn't provide: +

+ +
+ +
+

+ The caller always pays. When + accessing a public agent, you provide your own LLM API key—so LLM costs go to + your account. When accessing your own agent, your stored keys are used. +

- MCP endpoints for collections follow the same fork-to-use model. + Public collections can be accessed by any authenticated user who provides their own + environment variables.

- +

- No. Even if a collection is public, - you must be the owner to execute tools via MCP. Attempting to call another - user's collection returns: + As the owner, just make MCP calls—your stored env vars are used:

- +

- The caller (API key owner) pays for - tool execution. Usage is tracked against your account and counts against your rate - limits. + You must provide environment variables in the env field of{' '} + params:

-
-
-

You Provide

-
    -
  • • Your TPMJS API key (for auth)
  • -
  • • Your rate limit quota
  • -
-
-
-

Collection Provides

-
    -
  • • Tool environment variables
  • -
  • • Executor configuration
  • -
-
-
-
- -

- Tools use the collection owner's{' '} - stored environment variables—not yours. You cannot pass custom env vars via the - MCP request. -

-
-

- Example: If you fork a web scraping - collection, you must add your own FIRECRAWL_API_KEY in the collection settings. - The original owner's key is not copied. -

-
+

- If tools require environment variables that aren't configured, the behavior - depends on the tool. Most tools will return an error in the result: + If you don't provide required env vars:

-

- Check each tool's required environment variables and add them in your - collection's Env Vars tab. -

- -
-
-
- - 1 - - Find a public collection -
-

- Browse at tpmjs.com/{'{username}'}/collections/{'{slug}'} -

-
-
-
- - 2 - - Fork it to your account -
-

- Click "Fork" to create your own copy -

-
-
-
- - 3 - - Add environment variables -
-

- Configure tool API keys (not copied during fork) -

-
-
-
- - 4 - - - Use your fork via MCP - -
-

- Connect to /api/mcp/{'{your-username}'}/{'{slug}'}/http -

-
+ +
+

+ The caller always pays. When + accessing a public collection, you provide your own tool API keys—so tool costs + (if any) go to your accounts. Rate limits are tracked against your TPMJS API + key. +

@@ -1940,13 +1886,13 @@ X-RateLimit-Reset: 1704067200`}
- 403 + 404 - Fork Required + Not Found / Private

- You're trying to use someone else's agent/collection. Fork it first, - then use your own copy. + Resource doesn't exist or is private. Non-owners can only access PUBLIC + agents/collections.

@@ -1957,8 +1903,20 @@ X-RateLimit-Reset: 1704067200`} Missing Provider Key

- Agent's LLM provider key not configured. Add it at Dashboard → Settings → - API Keys. + For public agents: provide providerApiKey in your request. For your + own agents: add your LLM key in Dashboard → Settings. +

+
+
+
+ + 400 + + Missing Env Vars +
+

+ Tools require environment variables you didn't provide. Check the error + details for the list of required vars.

diff --git a/apps/web/src/lib/agents/build-tools.ts b/apps/web/src/lib/agents/build-tools.ts index 734d007..a4feb0c 100644 --- a/apps/web/src/lib/agents/build-tools.ts +++ b/apps/web/src/lib/agents/build-tools.ts @@ -237,9 +237,14 @@ function mergeEnvVars( * - Collection env vars are used as base * - Agent env vars override collection env vars for same keys * - Both are merged together for unique keys + * + * @param agent - The agent with tool relations + * @param callerEnvVars - Optional env vars provided by the caller (for non-owners accessing public agents). + * When provided, these are used INSTEAD of the agent's stored env vars. */ export function buildAgentTools( - agent: AgentWithRelations + agent: AgentWithRelations, + callerEnvVars?: Record ): Record> { const tools: Record> = {}; const seenTools = new Set(); @@ -247,8 +252,10 @@ export function buildAgentTools( // Parse agent-level executor config const agentExecutorConfig = parseExecutorConfig(agent.executorType, agent.executorConfig); - // Parse agent-level env vars - const agentEnvVars = parseEnvVars(agent.envVars); + // When callerEnvVars is provided (non-owner using public agent), use those exclusively. + // Otherwise, use the agent's stored env vars. + const useCallerEnvVars = callerEnvVars !== undefined; + const agentEnvVars = useCallerEnvVars ? callerEnvVars : parseEnvVars(agent.envVars); // Add tools from collections first for (const agentCollection of agent.collections) { @@ -264,7 +271,8 @@ export function buildAgentTools( const resolvedConfig = resolveExecutorConfig(agentExecutorConfig, collectionExecutorConfig); // Parse collection-level env vars and merge with agent env vars - const collectionEnvVars = parseEnvVars(collection.envVars); + // When callerEnvVars is provided, skip collection env vars entirely + const collectionEnvVars = useCallerEnvVars ? {} : parseEnvVars(collection.envVars); const mergedEnvVars = mergeEnvVars(collectionEnvVars, agentEnvVars); for (const collectionTool of collection.tools) { diff --git a/apps/web/src/lib/agents/env-helpers.ts b/apps/web/src/lib/agents/env-helpers.ts new file mode 100644 index 0000000..802d9f5 --- /dev/null +++ b/apps/web/src/lib/agents/env-helpers.ts @@ -0,0 +1,156 @@ +/** + * Helper functions for working with environment variables in agents and collections + */ + +import type { Agent, AgentCollection, AgentTool, Collection, Package, Tool } from '@tpmjs/db'; +import { prisma } from '@tpmjs/db'; +import type { TpmjsEnv } from '@tpmjs/types/tpmjs'; + +// Reuse the AgentWithRelations type +type AgentWithRelations = Agent & { + collections: (AgentCollection & { + collection: Collection & { + tools: Array<{ + tool: Tool & { package: Package }; + }>; + }; + })[]; + tools: (AgentTool & { + tool: Tool & { package: Package }; + })[]; +}; + +/** + * Get all required environment variables for an agent's tools + * Collects env vars from all tools in collections and individual tools + */ +export function getRequiredEnvVarsForAgent(agent: AgentWithRelations): TpmjsEnv[] { + const envVars = new Map(); + + // Collect from collections + for (const agentCollection of agent.collections) { + for (const collectionTool of agentCollection.collection.tools) { + const packageEnv = collectionTool.tool.package.env as TpmjsEnv[] | null; + if (packageEnv && Array.isArray(packageEnv)) { + for (const env of packageEnv) { + if (!envVars.has(env.name)) { + envVars.set(env.name, env); + } + } + } + } + } + + // Collect from individual tools + for (const agentTool of agent.tools) { + const packageEnv = agentTool.tool.package.env as TpmjsEnv[] | null; + if (packageEnv && Array.isArray(packageEnv)) { + for (const env of packageEnv) { + if (!envVars.has(env.name)) { + envVars.set(env.name, env); + } + } + } + } + + return Array.from(envVars.values()); +} + +/** + * Get required environment variables for a specific tool in a collection + */ +export async function getRequiredEnvVarsForCollectionTool( + collectionId: string, + toolName: string +): Promise { + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + include: { + tools: { + include: { + tool: { + include: { package: true }, + }, + }, + }, + }, + }); + + if (!collection) return []; + + // Parse tool name to find the matching tool + // Tool names are in format: sanitized-package-name-toolName + // e.g., "tpmjs-unsandbox-executeCode" + for (const ct of collection.tools) { + const pkgName = ct.tool.package.npmPackageName; + // Sanitize the package name the same way as in tool-converter + const sanitizedPkg = pkgName.replace(/[@/]/g, '-').replace(/^-+/, ''); + const expectedToolName = `${sanitizedPkg}-${ct.tool.name}`; + + if (toolName === expectedToolName || toolName.includes(sanitizedPkg)) { + const packageEnv = ct.tool.package.env as TpmjsEnv[] | null; + return packageEnv || []; + } + } + + return []; +} + +/** + * Get all required environment variables for all tools in a collection + */ +export async function getRequiredEnvVarsForCollection(collectionId: string): Promise { + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + include: { + tools: { + include: { + tool: { + include: { package: true }, + }, + }, + }, + }, + }); + + if (!collection) return []; + + const envVars = new Map(); + + for (const ct of collection.tools) { + const packageEnv = ct.tool.package.env as TpmjsEnv[] | null; + if (packageEnv && Array.isArray(packageEnv)) { + for (const env of packageEnv) { + if (!envVars.has(env.name)) { + envVars.set(env.name, env); + } + } + } + } + + return Array.from(envVars.values()); +} + +/** + * Check which required env vars are missing from provided env vars + * Returns only the required vars that don't have defaults and aren't provided + */ +export function getMissingEnvVars( + requiredEnvVars: TpmjsEnv[], + providedEnvVars: Record +): TpmjsEnv[] { + return requiredEnvVars.filter( + (env) => env.required !== false && !env.default && !providedEnvVars[env.name] + ); +} + +/** + * Validate that provided env var names are in the allowed list + * Returns invalid names that shouldn't be accepted + */ +export function getInvalidEnvVarNames( + providedEnvVars: Record, + allowedEnvNames: string[] +): string[] { + return Object.keys(providedEnvVars).filter((name) => !allowedEnvNames.includes(name)); +} diff --git a/apps/web/src/lib/mcp/handlers.ts b/apps/web/src/lib/mcp/handlers.ts index 70d1c7d..7a81a0a 100644 --- a/apps/web/src/lib/mcp/handlers.ts +++ b/apps/web/src/lib/mcp/handlers.ts @@ -1,4 +1,5 @@ import { prisma } from '@tpmjs/db'; +import type { TpmjsEnv } from '@tpmjs/types/tpmjs'; import { queueBridgeToolCall, waitForBridgeResult } from '~/app/api/bridge/route'; import { executeWithExecutor, parseExecutorConfig } from '../executors'; import { @@ -16,7 +17,7 @@ interface JsonRpcResponse { jsonrpc: '2.0'; id: JsonRpcId; result?: unknown; - error?: { code: number; message: string }; + error?: { code: number; message: string; data?: unknown }; } /** @@ -120,15 +121,19 @@ export async function handleToolsList( interface ToolsCallParams { name: string; arguments?: Record; + env?: Record; // Caller-provided env vars for non-owners } /** * Handle MCP tools/call request + * @param callerEnvVars - Optional env vars from caller (non-owner accessing public collection) + * When provided, these are used INSTEAD of collection's stored env vars */ export async function handleToolsCall( collectionId: string, params: ToolsCallParams, - requestId: JsonRpcId + requestId: JsonRpcId, + callerEnvVars?: Record ): Promise { try { const parsed = parseToolName(params.name); @@ -196,20 +201,49 @@ export async function handleToolsCall( const actualPackageName = collectionTool.tool.package.npmPackageName; const actualVersion = collectionTool.tool.package.npmVersion; + // When caller provides env vars (non-owner), validate required env vars + if (callerEnvVars !== undefined) { + const packageEnv = collectionTool.tool.package.env as TpmjsEnv[] | null; + if (packageEnv && Array.isArray(packageEnv)) { + const missingVars = packageEnv.filter( + (env) => env.required !== false && !env.default && !callerEnvVars[env.name] + ); + + if (missingVars.length > 0) { + return { + jsonrpc: '2.0', + id: requestId, + error: { + code: -32602, + message: 'Missing required environment variables', + data: { + missingVars: missingVars.map((e) => ({ + name: e.name, + description: e.description, + })), + }, + }, + }; + } + } + } + // Resolve executor configuration (collection config only for MCP - no agent context) const executorConfig = parseExecutorConfig( collection?.executorType, collection?.executorConfig ); - // Execute via resolved executor with collection's environment variables + // Execute via resolved executor + // Use caller-provided env vars if given (non-owner), otherwise use collection's stored env vars + const effectiveEnvVars = callerEnvVars ?? (collection?.envVars as Record) ?? {}; // Pass explicit version to avoid Deno HTTP import cache issues with @latest const result = await executeWithExecutor(executorConfig, { packageName: actualPackageName, name: parsed.toolName, version: actualVersion, params: params.arguments ?? {}, - env: (collection?.envVars as Record) ?? undefined, + env: Object.keys(effectiveEnvVars).length > 0 ? effectiveEnvVars : undefined, }); if (!result.success) { diff --git a/packages/types/src/agent.ts b/packages/types/src/agent.ts index 3857e66..c0d927f 100644 --- a/packages/types/src/agent.ts +++ b/packages/types/src/agent.ts @@ -126,6 +126,8 @@ export const CreateConversationSchema = z.object({ export const SendMessageSchema = z.object({ message: z.string().min(1, 'Message is required').max(50000, 'Message too long'), env: z.record(z.string(), z.string()).optional(), + // For non-owners accessing public agents: provide your own LLM API key + providerApiKey: z.string().optional(), }); // ============================================================================