@@ -7,30 +7,51 @@ import type { CostMode } from './model-config'
77 */
88export const FREE_COST_MODE = 'free' as const
99
10+ /**
11+ * Agents that are allowed to run in FREE mode.
12+ * Only these specific agents (and their expected models) get 0 credits in FREE mode.
13+ * This prevents abuse by users trying to use arbitrary agents for free.
14+ *
15+ * The mapping also specifies which models each agent is allowed to use in free mode.
16+ * If an agent uses a different model, it will be charged full credits.
17+ */
18+ export const FREE_MODE_AGENT_MODELS : Record < string , Set < string > > = {
19+ // Root orchestrator
20+ 'base2-free' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
21+
22+ // File exploration agents
23+ 'file-picker' : new Set ( [ 'google/gemini-2.5-flash-lite' ] ) ,
24+ 'file-picker-max' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
25+ 'file-lister' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
26+
27+ // Research agents
28+ 'researcher-web' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
29+ 'researcher-docs' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
30+
31+ // Command execution
32+ 'commander-lite' : new Set ( [ 'x-ai/grok-4.1-fast' ] ) ,
33+
34+ // Editor for free mode
35+ 'editor-glm' : new Set ( [ 'z-ai/glm-4.7' , 'z-ai/glm-4.6' ] ) ,
36+ }
37+
38+ /**
39+ * Set of all agent IDs allowed in FREE mode.
40+ * Derived from FREE_MODE_AGENT_MODELS for quick lookups.
41+ */
42+ export const FREE_MODE_ALLOWED_AGENTS = new Set ( Object . keys ( FREE_MODE_AGENT_MODELS ) )
43+
1044/**
1145 * Models that are allowed in FREE mode.
12- * Only these cheap/fast models get 0 credits in FREE mode .
46+ * Derived from FREE_MODE_AGENT_MODELS - this is the union of all allowed models .
1347 * This prevents abuse by users trying to use expensive models for free.
1448 */
15- export const FREE_MODE_ALLOWED_MODELS = new Set ( [
16- // Grok models used by base2-free, commander-lite, file-lister, file-picker-max
17- 'x-ai/grok-4.1-fast' ,
18- 'x-ai/grok-4-fast' , // researcher agents
19-
20- // Gemini flash models used by file-picker and other subagents
21- 'google/gemini-2.5-flash' ,
22- 'google/gemini-2.5-flash-lite' ,
23- 'google/gemini-2.5-flash-preview-09-2025' ,
24- 'google/gemini-2.5-flash-lite-preview-09-2025' ,
25-
26- // GPT models used by editor-gpt-5, thinker, context-pruner
27- 'openai/gpt-5.1' ,
28- 'openai/gpt-5.1-chat' ,
29- 'openai/gpt-5-mini' ,
30- ] )
49+ export const FREE_MODE_ALLOWED_MODELS = new Set (
50+ Object . values ( FREE_MODE_AGENT_MODELS ) . flatMap ( ( models ) => Array . from ( models ) ) ,
51+ )
3152
3253/**
33- * Agents that don't charge credits.
54+ * Agents that don't charge credits when credits would be very small (<5) .
3455 *
3556 * These are typically lightweight utility agents that:
3657 * - Use cheap models (e.g., Gemini Flash)
@@ -39,6 +60,10 @@ export const FREE_MODE_ALLOWED_MODELS = new Set([
3960 *
4061 * Making them free avoids user confusion when they connect their own
4162 * Claude subscription (BYOK) but still see credit charges for non-Claude models.
63+ *
64+ * NOTE: This is separate from FREE_MODE_ALLOWED_AGENTS which is for the
65+ * explicit "free" cost mode. These agents get free credits only when
66+ * the cost would be trivial (<5 credits).
4267 */
4368export const FREE_TIER_AGENTS = new Set ( [
4469 'file-picker' ,
@@ -65,13 +90,83 @@ export function isFreeModeAllowedModel(model: string): boolean {
6590}
6691
6792/**
68- * Check if an agent should be free (no credit charge).
93+ * Check if an agent is allowed to run in FREE mode.
94+ * Validates both the agent ID and optionally the publisher.
95+ *
96+ * For security, we only allow:
97+ * - Internal agents (no publisher, e.g., 'base2-free')
98+ * - Codebuff-published agents (publisher === 'codebuff')
99+ *
100+ * This prevents attackers from creating agents with matching names
101+ * under different publishers to abuse free mode.
102+ */
103+ export function isFreeModeAllowedAgent ( fullAgentId : string ) : boolean {
104+ const { publisherId, agentId } = parseAgentId ( fullAgentId )
105+
106+ // Must have a valid agent ID
107+ if ( ! agentId ) return false
108+
109+ // Must be in the allowed agents list
110+ if ( ! FREE_MODE_ALLOWED_AGENTS . has ( agentId ) ) return false
111+
112+ // Must be either internal (no publisher) or from codebuff
113+ if ( publisherId && publisherId !== 'codebuff' ) return false
114+
115+ return true
116+ }
117+
118+ /**
119+ * Check if a specific agent is allowed to use a specific model in FREE mode.
120+ * This is the strictest check - validates both the agent AND model combination.
121+ *
122+ * Returns true only if:
123+ * 1. The agent is allowed in free mode (isFreeModeAllowedAgent)
124+ * 2. The model is in that agent's allowed model set
125+ */
126+ export function isFreeModeAllowedAgentModel (
127+ fullAgentId : string ,
128+ model : string ,
129+ ) : boolean {
130+ // First check if agent is allowed in free mode (includes publisher validation)
131+ if ( ! isFreeModeAllowedAgent ( fullAgentId ) ) return false
132+
133+ // Parse to get the base agent ID for model lookup
134+ const { agentId } = parseAgentId ( fullAgentId )
135+ if ( ! agentId ) return false
136+
137+ // Get the allowed models for this agent
138+ const allowedModels = FREE_MODE_AGENT_MODELS [ agentId ]
139+ if ( ! allowedModels ) return false
140+
141+ // Empty set means programmatic agent (no LLM calls expected)
142+ // For these, any model check should fail (they shouldn't be making LLM calls)
143+ if ( allowedModels . size === 0 ) return false
144+
145+ return allowedModels . has ( model )
146+ }
147+
148+ /**
149+ * Check if an agent should be free (no credit charge) for small requests.
150+ * This is separate from FREE mode - these agents get free credits only
151+ * when the cost would be trivial (<5 credits).
152+ *
69153 * Handles all agent ID formats:
70154 * - 'file-picker'
71155 * - 'file-picker@1.0.0'
72156 * - 'codebuff/file-picker@0.0.2'
73157 */
74158export function isFreeAgent ( fullAgentId : string ) : boolean {
75- const { agentId } = parseAgentId ( fullAgentId )
76- return agentId ? FREE_TIER_AGENTS . has ( agentId ) : false
159+ const { publisherId, agentId } = parseAgentId ( fullAgentId )
160+
161+ // Must have a valid agent ID
162+ if ( ! agentId ) return false
163+
164+ // Must be in the free tier agents list
165+ if ( ! FREE_TIER_AGENTS . has ( agentId ) ) return false
166+
167+ // Must be either internal (no publisher) or from codebuff
168+ // This prevents publisher spoofing attacks
169+ if ( publisherId && publisherId !== 'codebuff' ) return false
170+
171+ return true
77172}
0 commit comments