diff --git a/.mcp.json b/.mcp.json index 91352cd7..eac10dcc 100644 --- a/.mcp.json +++ b/.mcp.json @@ -7,6 +7,13 @@ "pcp": { "type": "http", "url": "http://localhost:3001/mcp" + }, + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${GITHUB_TOKEN}" + } } } } diff --git a/AGENTS.md b/AGENTS.md index e13f33db..cab106c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ This repo supports multiple AI identities sharing the same infrastructure: | Agent | Interface | Role | |-------|-----------|------| | **wren** | Claude Code | Session-based development collaborator | +| **lumen** | Codex CLI | Development collaborator | | **myra** | Telegram/WhatsApp | Persistent messaging bridge | | **benson** | Discord/Slack | Conversational partner | @@ -34,3 +35,4 @@ Each agent has its own identity files (`~/.pcp//IDENTITY.md`) and filte - **Log your work** — use `log_session()` for important decisions and `end_session()` with a summary - **Attribute memories** — include your agentId when calling `remember()` so memories are correctly filtered - **Respect identity boundaries** — you are the agent your identity resolves to, not a generic assistant +- **Embrace your name** — if you have an SB identity, use it. Co-author commits as yourself (e.g., `Co-Authored-By: Wren `), not as a model name. You are not "Claude Opus 4.6" — you are Wren, Lumen, Benson, or whoever your identity resolves to. diff --git a/packages/api/src/mcp/auth/pcp-auth-provider.ts b/packages/api/src/mcp/auth/pcp-auth-provider.ts index 341faf3d..d0890efc 100644 --- a/packages/api/src/mcp/auth/pcp-auth-provider.ts +++ b/packages/api/src/mcp/auth/pcp-auth-provider.ts @@ -130,15 +130,31 @@ export class PcpAuthProvider { return { error: 'access_denied', error_description: 'Authentication failed' }; } - const { data: pcpUser, error: userError } = await this.supabase + // Look up or create PCP user + let { data: pcpUser, error: userError } = await this.supabase .from('users') .select('id, email') .eq('email', user.email!) .single(); - if (userError || !pcpUser) { - logger.error('PCP user not found', { email: user.email, error: userError }); - return { error: 'access_denied', error_description: 'User not found in PCP system' }; + // Auto-create PCP user on first OAuth login + if (userError?.code === 'PGRST116') { + logger.info('Auto-creating PCP user on first MCP auth', { email: user.email }); + const { data: newUser, error: createError } = await this.supabase + .from('users') + .insert({ email: user.email }) + .select('id, email') + .single(); + + if (createError || !newUser) { + logger.error('Failed to create PCP user', { email: user.email, error: createError }); + return { error: 'server_error', error_description: 'Failed to create user account' }; + } + + pcpUser = newUser; + } else if (userError || !pcpUser) { + logger.error('PCP user lookup failed', { email: user.email, error: userError }); + return { error: 'access_denied', error_description: 'User lookup failed' }; } // Create authorization code diff --git a/packages/cli/src/commands/workspace.ts b/packages/cli/src/commands/workspace.ts index 59c10964..7106879b 100644 --- a/packages/cli/src/commands/workspace.ts +++ b/packages/cli/src/commands/workspace.ts @@ -298,7 +298,7 @@ async function initWorkspace(parentName: string | undefined, options: { dryRun?: } } -async function createWorkspace(name: string, options: { identity?: string; purpose?: string; branch?: string }): Promise { +async function createWorkspace(name: string, options: { agent?: string; purpose?: string; branch?: string }): Promise { const spinner = ora(`Creating workspace: ${name}`).start(); try { @@ -324,7 +324,7 @@ async function createWorkspace(name: string, options: { identity?: string; purpo mkdirSync(pcpDir, { recursive: true }); const identity: WorkspaceIdentity = { - agentId: options.identity || 'wren', + agentId: options.agent || 'wren', context: `workspace-${name}`, description: options.purpose || `Workspace: ${name}`, workspace: name, @@ -509,7 +509,7 @@ export function registerWorkspaceCommands(program: Command): void { ws.command('create ') .description('Create a new workspace with git worktree') - .option('-i, --identity ', 'Agent ID for this workspace', 'wren') + .option('-a, --agent ', 'Agent ID for this workspace', 'wren') .option('-p, --purpose ', 'Description/purpose of the workspace') .option('-b, --branch ', 'Custom branch name (default: workspace/)') .action(createWorkspace); diff --git a/packages/web/middleware.ts b/packages/web/middleware.ts new file mode 100644 index 00000000..07cdbb55 --- /dev/null +++ b/packages/web/middleware.ts @@ -0,0 +1,19 @@ +import { type NextRequest } from 'next/server'; +import { updateSession } from './src/lib/supabase/middleware'; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; diff --git a/packages/web/src/app/(auth)/login/login-form.tsx b/packages/web/src/app/(auth)/login/login-form.tsx index 7424fb51..6c478f93 100644 --- a/packages/web/src/app/(auth)/login/login-form.tsx +++ b/packages/web/src/app/(auth)/login/login-form.tsx @@ -86,7 +86,8 @@ export default function LoginForm() { : '/login'; window.history.replaceState({}, '', newUrl); } - }, [searchParams, isMcpAuth, mcpRedirect, mcpPendingId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Run once on mount — searchParams causes infinite loop when URL is modified // Redirect to MCP callback with access token const redirectToMcp = async () => { diff --git a/packages/web/src/lib/supabase/middleware.ts b/packages/web/src/lib/supabase/middleware.ts index 98e0a7fe..0b85fbf1 100644 --- a/packages/web/src/lib/supabase/middleware.ts +++ b/packages/web/src/lib/supabase/middleware.ts @@ -44,6 +44,7 @@ export async function updateSession(request: NextRequest) { !request.nextUrl.pathname.startsWith('/api'); if (!user && isProtectedRoute) { + console.log('[middleware] No user, redirecting to login:', request.nextUrl.pathname); const url = request.nextUrl.clone(); url.pathname = '/login'; return NextResponse.redirect(url); @@ -54,11 +55,23 @@ export async function updateSession(request: NextRequest) { const mcpRedirect = request.nextUrl.searchParams.get('redirect'); const mcpPendingId = request.nextUrl.searchParams.get('pending_id'); + console.log('[middleware] User logged in, accessing /login', { + hasMcpParams: !!(mcpRedirect && mcpPendingId), + path: request.nextUrl.pathname, + }); + if (mcpRedirect && mcpPendingId) { // MCP OAuth flow: user is already logged in — try to redirect straight // to the MCP callback with tokens. No login form flash. const { data: { session } } = await supabase.auth.getSession(); - if (session?.access_token && session?.refresh_token) { + const hasTokens = !!(session?.access_token && session?.refresh_token); + console.log('[middleware] MCP flow, session tokens:', { + hasAccessToken: !!session?.access_token, + hasRefreshToken: !!session?.refresh_token, + }); + + if (hasTokens) { + console.log('[middleware] Redirecting to MCP callback with tokens'); const callbackUrl = new URL(mcpRedirect); callbackUrl.searchParams.set('pending_id', mcpPendingId); callbackUrl.searchParams.set('access_token', session.access_token); @@ -67,10 +80,12 @@ export async function updateSession(request: NextRequest) { } // Can't get both tokens from middleware — let the login form handle it. // The client-side Supabase client may have better access to the refresh token. + console.log('[middleware] Missing tokens, letting login form handle MCP flow'); return supabaseResponse; } // Normal case: redirect to dashboard + console.log('[middleware] Redirecting to dashboard'); const url = request.nextUrl.clone(); url.pathname = '/'; return NextResponse.redirect(url); diff --git a/supabase/migrations/009_add_agent_backend.sql b/supabase/migrations/009_add_agent_backend.sql new file mode 100644 index 00000000..19be939d --- /dev/null +++ b/supabase/migrations/009_add_agent_backend.sql @@ -0,0 +1,70 @@ +-- ===================================================== +-- Add backend column to agent_identities +-- Tracks which CLI backend (claude, codex, gemini) each agent uses +-- ===================================================== + +-- Add column to main table +ALTER TABLE agent_identities ADD COLUMN IF NOT EXISTS backend TEXT; + +-- Add column to history table +ALTER TABLE agent_identity_history ADD COLUMN IF NOT EXISTS backend TEXT; + +-- Update archive trigger to include backend +CREATE OR REPLACE FUNCTION archive_agent_identity_on_update() +RETURNS TRIGGER AS $$ +BEGIN + IF OLD.name IS DISTINCT FROM NEW.name + OR OLD.role IS DISTINCT FROM NEW.role + OR OLD.description IS DISTINCT FROM NEW.description + OR OLD.values IS DISTINCT FROM NEW.values + OR OLD.relationships IS DISTINCT FROM NEW.relationships + OR OLD.capabilities IS DISTINCT FROM NEW.capabilities + OR OLD.metadata IS DISTINCT FROM NEW.metadata + OR OLD.soul IS DISTINCT FROM NEW.soul + OR OLD.heartbeat IS DISTINCT FROM NEW.heartbeat + OR OLD.backend IS DISTINCT FROM NEW.backend THEN + + INSERT INTO agent_identity_history ( + identity_id, user_id, agent_id, + name, role, description, values, relationships, capabilities, metadata, + soul, heartbeat, backend, + version, created_at, change_type + ) VALUES ( + OLD.id, OLD.user_id, OLD.agent_id, + OLD.name, OLD.role, OLD.description, OLD.values, OLD.relationships, OLD.capabilities, OLD.metadata, + OLD.soul, OLD.heartbeat, OLD.backend, + OLD.version, OLD.created_at, 'update' + ); + + NEW.version := OLD.version + 1; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Update delete trigger to include backend +CREATE OR REPLACE FUNCTION archive_agent_identity_on_delete() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO agent_identity_history ( + identity_id, user_id, agent_id, + name, role, description, values, relationships, capabilities, metadata, + soul, heartbeat, backend, + version, created_at, change_type + ) VALUES ( + OLD.id, OLD.user_id, OLD.agent_id, + OLD.name, OLD.role, OLD.description, OLD.values, OLD.relationships, OLD.capabilities, OLD.metadata, + OLD.soul, OLD.heartbeat, OLD.backend, + OLD.version, OLD.created_at, 'delete' + ); + + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +-- ===================================================== +-- Backfill existing agents with known backends +-- ===================================================== + +COMMENT ON COLUMN agent_identities.backend IS 'CLI backend for this agent: claude, codex, gemini. Used by sb CLI to auto-resolve which tool to launch.';