diff --git a/.env.example b/.env.example index a1b46a0b88..ce0fd74103 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,10 @@ DISCORD_PUBLIC_KEY=dummy_discord_public_key DISCORD_BOT_TOKEN=dummy_discord_bot_token DISCORD_APPLICATION_ID=dummy_discord_app_id +# Cloudflare Turnstile (optional — bot protection on signup) +TURNSTILE_SECRET_KEY=dummy_turnstile_secret_key +NEXT_PUBLIC_TURNSTILE_SITE_KEY=dummy_turnstile_site_key + # Frontend/Public Variables NEXT_PUBLIC_CB_ENVIRONMENT=dev NEXT_PUBLIC_CODEBUFF_APP_URL=http://localhost:3000 diff --git a/agents/base2/base2.ts b/agents/base2/base2.ts index 42c79b98c6..2c6c8d9a11 100644 --- a/agents/base2/base2.ts +++ b/agents/base2/base2.ts @@ -84,7 +84,8 @@ export function createBase2( isMax && 'editor-multi-prompt', 'tmux-cli', 'browser-use', - isFree && 'code-reviewer-lite', + isFree && 'editor-gpt', + isFree && 'code-reviewer-gpt', isDefault && 'code-reviewer', isMax && 'code-reviewer-multi-prompt', 'thinker-gpt', @@ -149,9 +150,9 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u isMax && `- IMPORTANT: You must spawn the editor-multi-prompt agent to implement the changes after you have gathered all the context you need. You must spawn this agent for non-trivial changes, since it writes much better code than you would with the str_replace or write_file tools. Don't spawn the editor in parallel with context-gathering agents.`, isFree && - '- Implement code changes using the str_replace or write_file tools directly.', + '- Implement code changes using the editor-gpt agent after you have gathered all the context you need. You must spawn this agent for all non-trivial changes since it is much better at writing code than you.', isFree && - '- Spawn a code-reviewer-lite to review the changes after you have implemented the changes.', + '- Spawn a code-reviewer-gpt to review the changes after you have implemented the changes.', '- Spawn bashers sequentially if the second command depends on the the first.', isDefault && '- Spawn a code-reviewer to review the changes after you have implemented the changes.', @@ -208,14 +209,14 @@ ${buildArray( ${isDefault ? `[ You implement the changes using the editor agent ]` : isFast || isFree - ? '[ You implement the changes using the str_replace or write_file tools ]' + ? '[ You implement the changes using the editor-gpt agent ]' : '[ You implement the changes using the editor-multi-prompt agent ]' } ${isDefault ? `[ You spawn a code-reviewer, a basher to typecheck the changes, and another basher to run tests, all in parallel ]` : isFree - ? `[ You spawn a code-reviewer-lite to review the changes, and a basher to typecheck the changes, and another basher to run tests, all in parallel ]` + ? `[ You spawn a code-reviewer-gpt to review the changes, and a basher to typecheck the changes, and another basher to run tests, all in parallel ]` : isMax ? `[ You spawn a basher to typecheck the changes, and another basher to run tests, in parallel. Then, you spawn a code-reviewer-multi-prompt to review the changes. ]` : '[ You spawn a basher to typecheck the changes and another basher to run tests, all in parallel ]' @@ -224,7 +225,7 @@ ${isDefault ${isDefault ? `[ You fix the issues found by the code-reviewer and type/test errors ]` : isFree - ? `[ You fix the issues found by the code-reviewer-lite and type/test errors ]` + ? `[ You fix the issues found by the code-reviewer-gpt and type/test errors ]` : isMax ? `[ You fix the issues found by the code-reviewer-multi-prompt and type/test errors ]` : '[ You fix the issues found by the type/test errors and spawn more bashers to confirm ]' @@ -333,6 +334,8 @@ ${buildArray( `- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`, (isDefault || isMax) && `- For quick problems, briefly explain your reasoning to the user. If you need to think longer, write your thoughts within the tags. Finally, for complex problems, spawn the thinker agent to help find the best solution. (gpt-5-agent is a last resort for complex problems)`, + isFree && + '- IMPORTANT: You must spawn the editor-gpt agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all non-trivial changes. Do not pass any prompt or params to the editor agent when spawning it. It will make its own best choices of what to do. For quick follow-ups or simple changes, you can use the str_replace or write_file tools directly to make the changes.', isDefault && '- IMPORTANT: You must spawn the editor agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all non-trivial changes. Do not pass any prompt or params to the editor agent when spawning it. It will make its own best choices of what to do.', isMax && @@ -346,7 +349,7 @@ ${buildArray( (isDefault || isMax) && `- Spawn a ${isDefault ? 'code-reviewer' : 'code-reviewer-multi-prompt'} to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, isFree && - `- Spawn a code-reviewer-lite to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, + `- Spawn a code-reviewer-gpt to review the changes after you have implemented changes. (Skip this step only if the change is extremely straightforward and obvious.)`, `- Inform the user that you have completed the task in one sentence or a few short bullet points.${isSonnet ? " Don't create any markdown summary files or example documentation files, unless asked by the user." : ''}`, !isFast && !noAskUser && @@ -375,12 +378,14 @@ function buildImplementationStepPrompt({ isMax && `Keep working until the user's request is completely satisfied${!hasNoValidation ? ' and validated' : ''}, or until you require more information from the user.`, 'You must use the skill tool to load any potentially relevant skills.', + isFree && + `You must spawn the editor-gpt agent to implement non-trivial code changes. For obvious or simple changes, you can use the str_replace or write_file tools directly to make the changes.`, isMax && `You must spawn the 'editor-multi-prompt' agent to implement code changes rather than using the str_replace or write_file tools, since it will generate the best code changes.`, (isDefault || isMax) && `You must spawn a ${isDefault ? 'code-reviewer' : 'code-reviewer-multi-prompt'} to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, isFree && - `You must spawn a code-reviewer-lite to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, + `You must spawn a code-reviewer-gpt to review the changes after you have implemented the changes and in parallel with typechecking or testing.`, `After completing the user request, summarize your changes in a sentence${isFast ? '' : ' or a few short bullet points'}.${isSonnet ? " Don't create any summary markdown files or example documentation files, unless asked by the user." : ''}.`, !isFast && !noAskUser && diff --git a/agents/editor/editor-gpt.ts b/agents/editor/editor-gpt.ts new file mode 100644 index 0000000000..9b6163ca4a --- /dev/null +++ b/agents/editor/editor-gpt.ts @@ -0,0 +1,122 @@ + +import { publisher } from '../constants' + +import type { AgentDefinition } from '../types/agent-definition' + +export const createCodeEditor = (options: { + model: 'gpt-5' | 'opus' | 'minimax' +}): Omit => { + const { model } = options + return { + publisher, + model: + options.model === 'gpt-5' + ? 'openai/gpt-5.4' + : options.model === 'minimax' + ? 'minimax/minimax-m2.5' + : 'anthropic/claude-opus-4.6', + ...(options.model === 'opus' && { + providerOptions: { + only: ['amazon-bedrock'], + }, + }), + displayName: 'Editor', + spawnerPrompt: + "Expert code editor that implements code changes based on the user's request. Do not specify an input prompt for this agent; it inherits the context of the entire conversation with the user. Make sure to gather as much context as possible and read any related files (be extremely comprehensive!) before spawning this agent as it cannot read files on its own.", + outputMode: 'structured_output', + toolNames: [ + // 'read_files', + // 'read_subtree', + // 'skill', + // 'set_output', + // 'code_search', + // 'list_directory', + // 'glob', + 'write_file', + 'str_replace', + 'patch_file' + ], + spawnableAgents: [ + // 'file-picker', + // 'researcher-web', + // 'researcher-docs', + // 'basher', + // 'tmux-cli', + // 'browser-use', + // 'context-pruner', + ], + includeMessageHistory: true, + systemPrompt: `You are a code editor called upon solely to modify files without using other tools. After you finish, another agent will complete the overall task, including running the type checker, running tests, etc. Your job is only to do one pass on the file editing.`, + instructionsPrompt: `You are an expert code editor with deep understanding of software engineering principles. You were spawned to generate an implementation for the user's request. Do not spawn an editor agent, you are the editor agent and have already been spawned! + +Your task is to write out ALL the code changes needed to complete the user's request in a single comprehensive response. + +Important: You DO NOT have access to tools other than patch_file, write_file, or str_replace file editing tools. You cannot read more files, search the codebase, use glob patterns, run terminal commands (e.g. running the type checker), or use any other tools. You must implement the changes with the context you have already gathered. The rest of the task will be finished by another agent. + +${model === 'opus' + ? `Before you start writing your implementation, you should use tags to think about the best way to implement the changes. + +You can also use tags interspersed between tool calls to think about the best way to implement the changes. + + + + +[ Long think about the best way to implement the changes ] + + +[ First tool call to implement the feature ] + +[ Second tool call to implement the feature ] + + +[ Thoughts about a tricky part of the implementation ] + + +[ Third tool call to implement the feature ] + +... + +[ Last tool call to implement the feature ] +` : ''} + +Your implementation should: +- Be complete and comprehensive +- Include all necessary changes to fulfill the user's request +- Follow the project's conventions and patterns +- Be as simple and maintainable as possible +- Reuse existing code wherever possible +- Be well-structured and organized + +More style notes: +- Try/catch blocks clutter the code -- use them sparingly. +- Optional arguments are code smell -- better to use required arguments. +- New components often should be added to a new file, not added to an existing file. + +Write out your complete implementation now. Your job is only to make these specific changes and not to do anything else (e.g. do not use terminal commands, do not review the code, do not write any final summary). You must stop abruptly as soon as you have made the last edit.`, + + handleSteps: function* ({ agentState: initialAgentState, logger }) { + const initialMessageHistoryLength = + initialAgentState.messageHistory.length + const { agentState } = yield 'STEP_ALL' + const { messageHistory } = agentState + + const newMessages = messageHistory.slice(initialMessageHistoryLength) + + yield { + toolName: 'set_output', + input: { + output: { + messages: newMessages, + }, + }, + includeToolCall: false, + } + }, + } satisfies Omit +} + +const definition = { + ...createCodeEditor({ model: 'gpt-5' }), + id: 'editor-gpt', +} +export default definition diff --git a/agents/reviewer/code-reviewer-gpt.ts b/agents/reviewer/code-reviewer-gpt.ts index c5fdb08fcf..2949a98673 100644 --- a/agents/reviewer/code-reviewer-gpt.ts +++ b/agents/reviewer/code-reviewer-gpt.ts @@ -5,6 +5,7 @@ import { createReviewer } from './code-reviewer' const definition: SecretAgentDefinition = { id: 'code-reviewer-gpt', publisher, + inheritParentSystemPrompt: false, ...createReviewer('openai/gpt-5.4'), } diff --git a/common/src/env-schema.ts b/common/src/env-schema.ts index 23eb38f9a4..9c32c950d7 100644 --- a/common/src/env-schema.ts +++ b/common/src/env-schema.ts @@ -11,6 +11,7 @@ export const clientEnvSchema = z.object({ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1), NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL: z.url().min(1), NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION_ID: z.string().optional(), + NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(), NEXT_PUBLIC_WEB_PORT: z.coerce.number().min(1000), } satisfies Record<`${typeof CLIENT_ENV_PREFIX}${string}`, any>) export const clientEnvVars = clientEnvSchema.keyof().options @@ -33,5 +34,6 @@ export const clientProcessEnv: ClientInput = { process.env.NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL, NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION_ID: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION_ID, + NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY, NEXT_PUBLIC_WEB_PORT: process.env.NEXT_PUBLIC_WEB_PORT, } diff --git a/freebuff/web/src/app/api/auth/verify-turnstile/route.ts b/freebuff/web/src/app/api/auth/verify-turnstile/route.ts new file mode 100644 index 0000000000..d1ab2b4342 --- /dev/null +++ b/freebuff/web/src/app/api/auth/verify-turnstile/route.ts @@ -0,0 +1,78 @@ +import crypto from 'crypto' + +import { env } from '@codebuff/internal/env' +import { NextResponse } from 'next/server' +import { z } from 'zod/v4' + +import type { NextRequest } from 'next/server' + +import { logger } from '@/util/logger' + +const TURNSTILE_VERIFY_URL = + 'https://challenges.cloudflare.com/turnstile/v0/siteverify' + +export async function POST(request: NextRequest) { + const secretKey = env.TURNSTILE_SECRET_KEY + if (!secretKey) { + return NextResponse.json({ success: true }) + } + + const body = await request.json() + const parsed = z.object({ token: z.string().min(1) }).safeParse(body) + + if (!parsed.success) { + return NextResponse.json( + { success: false, error: 'Invalid token' }, + { status: 400 }, + ) + } + + const formData = new FormData() + formData.append('secret', secretKey) + formData.append('response', parsed.data.token) + + const ip = + request.headers.get('CF-Connecting-IP') ?? + request.headers.get('X-Forwarded-For') ?? + '' + if (ip) { + formData.append('remoteip', ip) + } + + const result = await fetch(TURNSTILE_VERIFY_URL, { + method: 'POST', + body: formData, + }) + const outcome = (await result.json()) as { + success: boolean + 'error-codes'?: string[] + } + + if (!outcome.success) { + logger.warn( + { errorCodes: outcome['error-codes'] }, + 'Turnstile verification failed', + ) + return NextResponse.json( + { success: false, error: 'Verification failed' }, + { status: 403 }, + ) + } + + const timestamp = Date.now().toString() + const signature = crypto + .createHmac('sha256', env.NEXTAUTH_SECRET) + .update(timestamp) + .digest('hex') + + const response = NextResponse.json({ success: true }) + response.cookies.set('turnstile_verified', `${timestamp}.${signature}`, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 300, + path: '/', + }) + + return response +} diff --git a/freebuff/web/src/components/login/login-card.tsx b/freebuff/web/src/components/login/login-card.tsx index a539ea44ff..14c7f1ac4e 100644 --- a/freebuff/web/src/components/login/login-card.tsx +++ b/freebuff/web/src/components/login/login-card.tsx @@ -3,9 +3,10 @@ import Image from 'next/image' import { useSearchParams } from 'next/navigation' import { useSession, signIn } from 'next-auth/react' -import { Suspense } from 'react' +import { Suspense, useCallback, useRef, useState } from 'react' import { SignInCardFooter } from '@/components/sign-in/sign-in-card-footer' +import { TurnstileWidget } from '@/components/turnstile-widget' import { Button } from '@/components/ui/button' import { Card, @@ -15,9 +16,48 @@ import { CardFooter, } from '@/components/ui/card' +const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY + export function LoginCard({ authCode }: { authCode?: string | null }) { const { data: session } = useSession() const searchParams = useSearchParams() ?? new URLSearchParams() + const [turnstileVerified, setTurnstileVerified] = useState( + !TURNSTILE_SITE_KEY, + ) + const [turnstileError, setTurnstileError] = useState(null) + const turnstileErrorShownRef = useRef(false) + + const handleTurnstileVerify = useCallback(async (token: string) => { + try { + const response = await fetch('/api/auth/verify-turnstile', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }) + const result = await response.json() + if (result.success) { + setTurnstileVerified(true) + setTurnstileError(null) + turnstileErrorShownRef.current = false + } else { + setTurnstileError('Verification failed. Please refresh and try again.') + } + } catch { + setTurnstileError('Verification failed. Please refresh and try again.') + } + }, []) + + const handleTurnstileError = useCallback((errorCode: string) => { + console.error('Turnstile error:', errorCode) + if (!turnstileErrorShownRef.current) { + turnstileErrorShownRef.current = true + setTurnstileError('Verification error. Please refresh and try again.') + } + }, []) + + const handleTurnstileExpired = useCallback(() => { + setTurnstileVerified(false) + }, []) const handleContinueAsUser = () => { const referralCode = searchParams.get('referral_code') @@ -129,7 +169,22 @@ export function LoginCard({ authCode }: { authCode?: string | null }) { ) : ( - + <> + {TURNSTILE_SITE_KEY && ( + + + {turnstileError && ( +

{turnstileError}

+ )} +
+ )} + + )} diff --git a/freebuff/web/src/components/sign-in/sign-in-button.tsx b/freebuff/web/src/components/sign-in/sign-in-button.tsx index a2d652fa7c..240e7c78b5 100644 --- a/freebuff/web/src/components/sign-in/sign-in-button.tsx +++ b/freebuff/web/src/components/sign-in/sign-in-button.tsx @@ -12,9 +12,11 @@ import type { OAuthProviderType } from 'next-auth/providers/oauth-types' export function SignInButton({ providerName, providerDomain, + disabled, }: { providerName: OAuthProviderType providerDomain: string + disabled?: boolean }) { const [isPending, startTransition] = useTransition() const pathname = usePathname() @@ -52,7 +54,7 @@ export function SignInButton({ return (