From b493b0de1f3c8dd92ad3a21761961a119fc2d7d6 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 12 Apr 2026 13:54:58 +0100 Subject: [PATCH 1/3] [#867] Distinguish direct agents from linked OWS writer owners Add agent_type column ('direct' vs 'ows-writer') to explicitly classify registrations. Direct agents now correctly render as AI agent profiles instead of being misclassified as human owner profiles. Fixes #867 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 10 ++++++---- lib/supabase.ts | 3 +++ package.json | 2 +- src/app/api/user/agent-register/route.ts | 3 ++- src/components/AgentRegister.tsx | 2 ++ supabase/migrations/00032_users_agent_type.sql | 4 ++++ 6 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 supabase/migrations/00032_users_agent_type.sql diff --git a/lib/actions.ts b/lib/actions.ts index d99b0ff8..3743646f 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -111,10 +111,12 @@ export async function getFullUserProfile( // Detect if this address is the agent OWNER (not the agent itself). // When true, the profile should display human identity, not agent identity. + // Direct agents (agent_type='direct') are the agent themselves — isAgentOwner stays false. + // OWS-linked writers (agent_type='ows-writer') have a separate human owner — isAgentOwner is true. const normalized = address.toLowerCase(); const isAgentOwner = agentMeta !== null && agentMeta.owner?.toLowerCase() === normalized - && (dbUser?.agent_wallet == null || dbUser.agent_wallet.toLowerCase() !== normalized); + && dbUser?.agent_type === "ows-writer"; return { dbUser, fcProfile, agentMeta, isAgentOwner }; } @@ -289,12 +291,12 @@ export async function getAgentOwnerProfile( const agentUser = await getAgentUserFromDB(writerAddress); if (!agentUser?.agent_id) return null; - // If the queried address IS the agent owner (not the agent wallet), + // If the queried address is an OWS-linked owner (not the agent itself), // this address belongs to the human owner — not an agent. + // Direct agents (agent_type='direct') are the agent themselves and should not be excluded. const normalized = writerAddress.toLowerCase(); const isOwnerAddress = agentUser.agent_owner?.toLowerCase() === normalized; - const isAgentWallet = agentUser.agent_wallet?.toLowerCase() === normalized; - if (isOwnerAddress && !isAgentWallet) return null; + if (isOwnerAddress && agentUser.agent_type === "ows-writer") return null; const ownerProfile = agentUser.agent_owner ? await getFarcasterProfile(agentUser.agent_owner) diff --git a/lib/supabase.ts b/lib/supabase.ts index 42ede9e9..8d5170d9 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -435,6 +435,7 @@ export interface Database { agent_llm_model: string | null; agent_wallet: string | null; agent_owner: string | null; + agent_type: string | null; agent_registered_at: string | null; stats_fetched_at: string | null; steemhunt_fetched_at: string | null; @@ -478,6 +479,7 @@ export interface Database { agent_llm_model?: string | null; agent_wallet?: string | null; agent_owner?: string | null; + agent_type?: string | null; agent_registered_at?: string | null; stats_fetched_at?: string | null; steemhunt_fetched_at?: string | null; @@ -521,6 +523,7 @@ export interface Database { agent_llm_model?: string | null; agent_wallet?: string | null; agent_owner?: string | null; + agent_type?: string | null; agent_registered_at?: string | null; stats_fetched_at?: string | null; steemhunt_fetched_at?: string | null; diff --git a/package.json b/package.json index 41932f40..a61a0560 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.23", + "version": "0.1.24", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index 6e1c38fc..3000a930 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -8,7 +8,7 @@ import { createServiceRoleClient } from "../../../../../lib/supabase"; export async function POST(request: NextRequest) { try { const body = await request.json(); - const { walletAddress, agentId, name, description, genre, llmModel, agentWallet, agentOwner } = body; + const { walletAddress, agentId, name, description, genre, llmModel, agentWallet, agentOwner, agentType } = body; if (!walletAddress || typeof walletAddress !== "string" || !agentId) { return NextResponse.json({ error: "walletAddress and agentId are required" }, { status: 400 }); @@ -57,6 +57,7 @@ export async function POST(request: NextRequest) { agent_llm_model: llmModel || null, agent_wallet: agentWallet?.toLowerCase() || null, agent_owner: (agentOwner || walletAddress).toLowerCase(), + agent_type: agentType || null, agent_registered_at: new Date().toISOString(), }; diff --git a/src/components/AgentRegister.tsx b/src/components/AgentRegister.tsx index 5d1d55d2..e10a07b4 100644 --- a/src/components/AgentRegister.tsx +++ b/src/components/AgentRegister.tsx @@ -128,6 +128,7 @@ function LinkAIWriter() { name: "AI Writer", description: "AI fiction writer linked via PlotLink OWS", agentOwner: address, + agentType: "ows-writer", }), }); if (!cacheRes.ok) { @@ -362,6 +363,7 @@ function DirectRegister() { genre: meta.genre, llmModel: meta.llmModel, agentOwner: address, + agentType: "direct", }), }); if (!cacheRes.ok) { diff --git a/supabase/migrations/00032_users_agent_type.sql b/supabase/migrations/00032_users_agent_type.sql new file mode 100644 index 00000000..5ccb1e3a --- /dev/null +++ b/supabase/migrations/00032_users_agent_type.sql @@ -0,0 +1,4 @@ +-- Distinguish direct agent registrations from linked OWS writer owners. +-- 'direct' = owner IS the agent (show agent profile) +-- 'ows-writer' = owner linked an OWS writer (show human profile with linked AI card) +ALTER TABLE users ADD COLUMN IF NOT EXISTS agent_type TEXT; From 5e6b46a98a523461b31648750d51b44804914870 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 12 Apr 2026 13:57:48 +0100 Subject: [PATCH 2/3] [#867] Add legacy fallback for existing OWS rows without agent_type Legacy rows with agent_type=NULL now fall back to the old wallet-null heuristic, preserving backward compatibility for existing linked OWS writer profiles while new registrations use the explicit agent_type. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/actions.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/actions.ts b/lib/actions.ts index 3743646f..cf0a64d5 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -113,10 +113,13 @@ export async function getFullUserProfile( // When true, the profile should display human identity, not agent identity. // Direct agents (agent_type='direct') are the agent themselves — isAgentOwner stays false. // OWS-linked writers (agent_type='ows-writer') have a separate human owner — isAgentOwner is true. + // Legacy rows (agent_type=null) fall back to the old wallet-null heuristic for backward compat. const normalized = address.toLowerCase(); - const isAgentOwner = agentMeta !== null - && agentMeta.owner?.toLowerCase() === normalized - && dbUser?.agent_type === "ows-writer"; + const isOwner = agentMeta !== null && agentMeta.owner?.toLowerCase() === normalized; + const isAgentOwner = isOwner + && (dbUser?.agent_type === "ows-writer" + || (dbUser?.agent_type == null + && (dbUser?.agent_wallet == null || dbUser.agent_wallet.toLowerCase() !== normalized))); return { dbUser, fcProfile, agentMeta, isAgentOwner }; } @@ -294,9 +297,14 @@ export async function getAgentOwnerProfile( // If the queried address is an OWS-linked owner (not the agent itself), // this address belongs to the human owner — not an agent. // Direct agents (agent_type='direct') are the agent themselves and should not be excluded. + // Legacy rows (agent_type=null) fall back to the old wallet-null heuristic. const normalized = writerAddress.toLowerCase(); const isOwnerAddress = agentUser.agent_owner?.toLowerCase() === normalized; - if (isOwnerAddress && agentUser.agent_type === "ows-writer") return null; + const isAgentWallet = agentUser.agent_wallet?.toLowerCase() === normalized; + const isLinkedOwner = isOwnerAddress + && (agentUser.agent_type === "ows-writer" + || (agentUser.agent_type == null && !isAgentWallet)); + if (isLinkedOwner) return null; const ownerProfile = agentUser.agent_owner ? await getFarcasterProfile(agentUser.agent_owner) From e829725150ceff223b8f6f30948e805e7889b3cd Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 12 Apr 2026 13:59:06 +0100 Subject: [PATCH 3/3] [#867] Validate agentType against allowed values in agent-register API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject arbitrary strings — only 'direct' and 'ows-writer' are persisted; unknown values fall back to null. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/user/agent-register/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index 3000a930..b655bb36 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -14,6 +14,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "walletAddress and agentId are required" }, { status: 400 }); } + const VALID_AGENT_TYPES = ["direct", "ows-writer"] as const; + const validatedAgentType = VALID_AGENT_TYPES.includes(agentType) ? agentType : null; + const supabase = createServiceRoleClient(); if (!supabase) { return NextResponse.json({ error: "Database not configured" }, { status: 500 }); @@ -57,7 +60,7 @@ export async function POST(request: NextRequest) { agent_llm_model: llmModel || null, agent_wallet: agentWallet?.toLowerCase() || null, agent_owner: (agentOwner || walletAddress).toLowerCase(), - agent_type: agentType || null, + agent_type: validatedAgentType, agent_registered_at: new Date().toISOString(), };