Canvas: Support Anthropic Claude based AI agents

Change-Id: Ib74c9672da9a80a4f20d63741a471c728a435b8e
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
index 1deeb7a..9568c7e 100644
--- a/apps/canvas/config/src/config.ts
+++ b/apps/canvas/config/src/config.ts
@@ -127,13 +127,18 @@
 						: {
 								enabled: false,
 							},
-					...(n.data.agent != null
-						? {
-								agent: {
-									geminiApiKey: n.data.agent.geminiApiKey,
-								},
-							}
-						: {}),
+					...(n.data.model?.name === "gemini" && {
+						model: {
+							name: "gemini",
+							geminiApiKey: n.data.model.apiKey,
+						},
+					}),
+					...(n.data.model?.name === "claude" && {
+						model: {
+							name: "claude",
+							anthropicApiKey: n.data.model.apiKey,
+						},
+					}),
 				};
 			});
 		return {
@@ -192,7 +197,7 @@
 	if (networks.length === 0) {
 		return ret;
 	}
-	const repoNodes = (config.service || [])
+	const repoNodes = [...(config.service || []), ...(config.agent || [])]
 		.filter((s) => s.source?.repository != null)
 		.map((s): GithubNode | null => {
 			const existing = current.nodes.find(
@@ -292,7 +297,12 @@
 				}),
 				volume: s.volume || [],
 				preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
-				agent: s.agent,
+				...(s.model != null && {
+					model:
+						s.model.name === "gemini"
+							? { name: "gemini", apiKey: s.model.geminiApiKey }
+							: { name: "claude", apiKey: s.model.anthropicApiKey },
+				}),
 				// TODO(gio): dev
 				isChoosingPortToConnect: false,
 			},
@@ -306,54 +316,55 @@
 						},
 		};
 	});
-	const serviceGateways = config.service?.flatMap((s, index): GatewayHttpsNode[] => {
-		return (s.ingress || []).map((i): GatewayHttpsNode => {
-			let existing: GatewayHttpsNode | null = null;
-			if (i.nodeId !== undefined) {
-				existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
-			}
-			console.log("!!!", i.network, networks);
-			return {
-				id: existing != null ? existing.id : uuidv4(),
-				type: "gateway-https",
-				data: {
-					label: i.subdomain,
-					envVars: [],
-					ports: [],
-					network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
-					subdomain: i.subdomain,
-					https: {
-						serviceId: services![index]!.id,
-						portId: services![index]!.data.ports.find((p) => {
-							const port = i.port;
-							if ("name" in port) {
-								return p.name === port.name;
-							} else {
-								return `${p.value}` === port.value;
-							}
-						})!.id,
+	const serviceGateways = [...(config.service || []), ...(config.agent || [])]?.flatMap(
+		(s, index): GatewayHttpsNode[] => {
+			return (s.ingress || []).map((i): GatewayHttpsNode => {
+				let existing: GatewayHttpsNode | null = null;
+				if (i.nodeId !== undefined) {
+					existing = current.nodes.find((n) => n.id === i.nodeId) as GatewayHttpsNode;
+				}
+				return {
+					id: existing != null ? existing.id : uuidv4(),
+					type: "gateway-https",
+					data: {
+						label: i.subdomain,
+						envVars: [],
+						ports: [],
+						network: networks.find((n) => n.name.toLowerCase() === i.network.toLowerCase())!.domain,
+						subdomain: i.subdomain,
+						https: {
+							serviceId: services![index]!.id,
+							portId: services![index]!.data.ports.find((p) => {
+								const port = i.port;
+								if ("name" in port) {
+									return p.name === port.name;
+								} else {
+									return `${p.value}` === port.value;
+								}
+							})!.id,
+						},
+						auth: i.auth.enabled
+							? {
+									enabled: true,
+									groups: i.auth.groups || [],
+									noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
+								}
+							: {
+									enabled: false,
+									groups: [],
+									noAuthPathPatterns: [],
+								},
 					},
-					auth: i.auth.enabled
-						? {
-								enabled: true,
-								groups: i.auth.groups || [],
-								noAuthPathPatterns: i.auth.noAuthPathPatterns || [],
-							}
-						: {
-								enabled: false,
-								groups: [],
-								noAuthPathPatterns: [],
-							},
-				},
-				position: {
-					x: 0,
-					y: 0,
-				},
-			};
-		});
-	});
+					position: {
+						x: 0,
+						y: 0,
+					},
+				};
+			});
+		},
+	);
 	const exposures = new Map<string, GatewayTCPNode>();
-	config.service
+	[...(config.service || []), ...(config.agent || [])]
 		?.flatMap((s, index): GatewayTCPNode[] => {
 			return (s.expose || []).map((e): GatewayTCPNode => {
 				let existing: GatewayTCPNode | null = null;
@@ -649,15 +660,20 @@
 			},
 		];
 	});
-	const repoEdges = (services || []).map((s): Edge => {
-		return {
-			id: uuidv4(),
-			source: s.data.repository!.repoNodeId!,
-			sourceHandle: "repository",
-			target: s.id,
-			targetHandle: "repository",
-		};
-	});
+	const repoEdges = (services || [])
+		.map((s): Edge | null => {
+			if (s.data.repository == null) {
+				return null;
+			}
+			return {
+				id: uuidv4(),
+				source: s.data.repository!.repoNodeId!,
+				sourceHandle: "repository",
+				target: s.id,
+				targetHandle: "repository",
+			};
+		})
+		.filter((e) => e != null);
 	ret.edges = [...repoEdges, ...envVarEdges, ...exposureEdges, ...ingressEdges];
 	return ret;
 }
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index 604bcc4..90523d5 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -163,8 +163,9 @@
 				codeServerNodeId: string;
 				sshNodeId: string;
 		  };
-	agent?: {
-		geminiApiKey?: string;
+	model?: {
+		name: "gemini" | "claude";
+		apiKey?: string;
 	};
 	info?: z.infer<typeof serviceAnalyzisSchema>;
 };
@@ -306,12 +307,19 @@
 });
 
 export const envSchema = z.object({
-	instanceId: z.optional(z.string().min(1)),
-	deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
-	networks: z.array(networkSchema).default([]),
+	deployKeyPublic: z.string().optional(),
+	instanceId: z.string().optional(),
+	networks: z.array(
+		z.object({
+			name: z.string(),
+			domain: z.string(),
+			hasAuth: z.boolean(),
+		}),
+	),
 	integrations: z.object({
 		github: z.boolean(),
 		gemini: z.boolean(),
+		anthropic: z.boolean(),
 	}),
 	services: z.array(serviceInfoSchema),
 	user: z.object({
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
index de0725a..e186f4b 100644
--- a/apps/canvas/config/src/types.ts
+++ b/apps/canvas/config/src/types.ts
@@ -54,6 +54,17 @@
 
 const ServiceTypeSchema = z.enum(ServiceTypes);
 
+const ModelSchema = z.discriminatedUnion("name", [
+	z.object({
+		name: z.literal("claude"),
+		anthropicApiKey: z.string().optional(),
+	}),
+	z.object({
+		name: z.literal("gemini"),
+		geminiApiKey: z.string().optional(),
+	}),
+]);
+
 const ServiceSchema = z.object({
 	nodeId: z.string().optional(),
 	type: ServiceTypeSchema,
@@ -94,15 +105,12 @@
 			codeServer: DomainSchema.optional(),
 		})
 		.optional(),
-	agent: z
-		.object({
-			geminiApiKey: z.string().optional(),
-		})
-		.optional(),
+	model: ModelSchema.optional(),
 });
 
 const AgentSchema = ServiceSchema.extend({
 	type: z.literal("sketch:latest"),
+	model: ModelSchema,
 });
 
 const VolumeTypeSchema = z.enum(["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]);
@@ -146,6 +154,7 @@
 		})
 		.optional(),
 	geminiApiKey: z.string().optional(),
+	anthropicApiKey: z.string().optional(),
 });
 
 export const ConfigWithInputSchema = ConfigSchema.extend({