Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
}
}
}
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -34,3 +35,4 @@ Each agent has its own identity files (`~/.pcp/<agentId>/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 <noreply@anthropic.com>`), not as a model name. You are not "Claude Opus 4.6" — you are Wren, Lumen, Benson, or whoever your identity resolves to.
24 changes: 20 additions & 4 deletions packages/api/src/mcp/auth/pcp-auth-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
async function createWorkspace(name: string, options: { agent?: string; purpose?: string; branch?: string }): Promise<void> {
const spinner = ora(`Creating workspace: ${name}`).start();

try {
Expand All @@ -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,
Expand Down Expand Up @@ -509,7 +509,7 @@ export function registerWorkspaceCommands(program: Command): void {

ws.command('create <name>')
.description('Create a new workspace with git worktree')
.option('-i, --identity <agent>', 'Agent ID for this workspace', 'wren')
.option('-a, --agent <agent>', 'Agent ID for this workspace', 'wren')
.option('-p, --purpose <desc>', 'Description/purpose of the workspace')
Comment on lines 510 to 513

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the workspace create flag from --identity/-i to --agent/-a is a breaking CLI interface change (and the repo docs still reference --identity). Consider supporting --identity as a deprecated alias (mapping it to the same option) for backward compatibility, or clearly documenting the breaking change and updating all references in the same PR.

Copilot uses AI. Check for mistakes.
.option('-b, --branch <branch>', 'Custom branch name (default: workspace/<name>)')
.action(createWorkspace);
Expand Down
19 changes: 19 additions & 0 deletions packages/web/middleware.ts
Original file line number Diff line number Diff line change
@@ -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)$).*)',
],
};
3 changes: 2 additions & 1 deletion packages/web/src/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
17 changes: 16 additions & 1 deletion packages/web/src/lib/supabase/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
70 changes: 70 additions & 0 deletions supabase/migrations/009_add_agent_backend.sql
Original file line number Diff line number Diff line change
@@ -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

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration section header says it will "Backfill existing agents with known backends", but no backfill statement is present (only a column comment). Either add the intended UPDATE/INSERT backfill, or remove/rename the section to avoid misleading future readers/operators.

Suggested change
-- Backfill existing agents with known backends
-- Document backend column

Copilot uses AI. Check for mistakes.
-- =====================================================

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.';