Canvas: Implement Agent Sketch node, update dodo-app.jsonschema

- Add Gemini API key to the project
- Update dodo schema to support Gemini API key
- Update dodo schema to support Agent Sketch node

Change-Id: I6a96186f86ad169152ca0021b38130e485ebbf14
diff --git a/apps/canvas/back/prisma/migrations/20250623163606_gemini_api_key/migration.sql b/apps/canvas/back/prisma/migrations/20250623163606_gemini_api_key/migration.sql
new file mode 100644
index 0000000..4efc568
--- /dev/null
+++ b/apps/canvas/back/prisma/migrations/20250623163606_gemini_api_key/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "geminiApiKey" TEXT;
diff --git a/apps/canvas/back/prisma/schema.prisma b/apps/canvas/back/prisma/schema.prisma
index 8775a15..07482f3 100644
--- a/apps/canvas/back/prisma/schema.prisma
+++ b/apps/canvas/back/prisma/schema.prisma
@@ -14,14 +14,15 @@
 }
 
 model Project {
-  id Int @id @default(autoincrement())
-  userId String
-  name String
-  state String?
-  draft String?
-  instanceId String?
-  deployKey String?
-  githubToken String?
-  access String?
+  id              Int     @id @default(autoincrement())
+  userId          String
+  name            String
+  state           String?
+  draft           String?
+  instanceId      String?
+  deployKey       String?
   deployKeyPublic String?
+  githubToken     String?
+  access          String?
+  geminiApiKey     String?
 }
\ No newline at end of file
diff --git a/apps/canvas/back/src/dodo-app.jsonschema b/apps/canvas/back/src/dodo-app.jsonschema
index df6553d..bbb8cee 100644
--- a/apps/canvas/back/src/dodo-app.jsonschema
+++ b/apps/canvas/back/src/dodo-app.jsonschema
@@ -10,6 +10,12 @@
                 "$ref": "#/definitions/Service"
             }
         },
+        "agent": {
+            "type": "array",
+            "items": {
+                "$ref": "#/definitions/Agent"
+            }
+        },
         "volume": {
             "type": "array",
             "items": {
@@ -106,6 +112,10 @@
                     "properties": {
                         "port": {
                             "$ref": "#/definitions/PortValue"
+                        },
+                        "nodeId": {
+                            "type": "string",
+                            "description": "Identifier of the node this resource is assigned to."
                         }
                     },
                     "required": ["port"]
@@ -194,6 +204,10 @@
                 },
                 "auth": {
                     "$ref": "#/definitions/Auth"
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
                 }
             },
             "required": ["network", "subdomain", "port", "auth"]
@@ -237,7 +251,7 @@
                         "properties": {
                             "name": {
                                 "type": "string",
-                                "description": "Name of the port (e.g., 'http', 'grpc')."
+                                "description": "Name of the port (e.g., 'http', 'grpc'). Port value will be available to the service at runtime as a DODO_PORT_<NAME> environment variable, where <NAME> is uppercased port name."
                             },
                             "value": {
                                 "type": "number",
@@ -325,10 +339,150 @@
                         }
                     },
                     "required": ["enabled"]
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
                 }
             },
             "required": ["type", "name", "source"]
         },
+        "Agent": {
+            "type": "object",
+            "description": "AI Agent definition, which user can communicate with to implement new service or add new features to already existing one.",
+            "properties": {
+                "name": {
+                    "type": "string",
+                    "description": "Name of the AI Agent."
+                },
+                "geminiApiKey": {
+                    "type": "string",
+                    "description": "Gemini API Key"
+                },
+                "source": {
+                    "type": "object",
+                    "description": "If provided, defines where to pull the source code from.",
+                    "properties": {
+                        "repository": {
+                            "type": "string",
+                            "format": "uri",
+                            "description": "SSH URL of the Git repository."
+                        },
+                        "branch": {
+                            "type": "string",
+                            "description": "Branch to deploy from."
+                        },
+                        "rootDir": {
+                            "type": "string",
+                            "description": "Root directory within the repository for this service."
+                        }
+                    },
+                    "required": ["repository", "branch", "rootDir"]
+                },
+                "ports": {
+                    "type": "array",
+                    "description": "List of ports this service exposes when started.",
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "description": "Name of the port (e.g., 'http', 'grpc'). Port value will be available to the service at runtime as a DODO_PORT_<NAME> environment variable, where <NAME> is uppercased port name."
+                            },
+                            "value": {
+                                "type": "number",
+                                "description": "Port number."
+                            },
+                            "protocol": {
+                                "type": "string",
+                                "enum": ["TCP", "UDP"]
+                            }
+                        },
+                        "required": ["name", "value", "protocol"]
+                    }
+                },
+                "env": {
+                    "type": "array",
+                    "description": "List of environment variables.",
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "description": "Name of the environment variable as used by the service."
+                            },
+                            "alias": {
+                                "type": "string",
+                                "description": "Original name of the environment variable if aliased."
+                            }
+                        },
+                        "required": ["name"]
+                    }
+                },
+                "ingress": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/Ingress"
+                    },
+                    "description": "HTTPS ingress definitions for this service."
+                },
+                "expose": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/PortDomain"
+                    },
+                    "description": "TCP/UDP exposure definitions for this service."
+                },
+                "volume": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    },
+                    "description": "Names of volumes to be mounted to this service."
+                },
+                "preBuildCommands": {
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "bin": {
+                                "type": "string",
+                                "description": "A command to run before building/starting the service."
+                            }
+                        },
+                        "required": ["bin"]
+                    }
+                },
+                "dev": {
+                    "type": "object",
+                    "description": "Describes to run this service in development mode or not.",
+                    "properties": {
+                        "enabled": {
+                            "type": "boolean",
+                            "description": "Whether development mode is enabled for this service."
+                        },
+                        "username": {
+                            "type": "string",
+                            "description": "Username for SSH/Code-server access in dev mode."
+                        },
+                        "ssh": {
+                            "$ref": "#/definitions/Domain",
+                            "description": "Network exposure for SSH in dev mode."
+                        },
+                        "codeServer": {
+                            "$ref": "#/definitions/Domain",
+                            "description": "Network exposure for Code-server in dev mode."
+                        }
+                    },
+                    "required": ["enabled"]
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
+                }
+            },
+            "required": ["name"]
+        },
         "Volume": {
             "type": "object",
             "description": "Volume definition which can be mounted to services and other infrastructure components. When mounted to the service, it's mount location is exposed as DODO_VOLUME_<NAME> env variable where <NAME> represents name of the volume (name is upper cased).",
@@ -344,6 +498,10 @@
                     "type": "string",
                     "pattern": "^[0-9]+(Gi|Mi|Ti)$",
                     "description": "Size of the volume (e.g., '1Gi', '500Mi')."
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
                 }
             },
             "required": ["name", "accessMode", "size"]
@@ -367,6 +525,10 @@
                         "$ref": "#/definitions/PortDomain"
                     },
                     "description": "Network exposure definitions for this PostgreSQL instance."
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
                 }
             },
             "required": ["name", "size"]
@@ -390,9 +552,13 @@
                         "$ref": "#/definitions/PortDomain"
                     },
                     "description": "Network exposure definitions for this MongoDB instance."
+                },
+                "nodeId": {
+                    "type": "string",
+                    "description": "Identifier of the node this resource is assigned to."
                 }
             },
             "required": ["name", "size"]
         }
     }
-}
+}
\ No newline at end of file
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 82e55fe..4190f70 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -322,6 +322,7 @@
 				deployKey: true,
 				deployKeyPublic: true,
 				state: true,
+				geminiApiKey: true,
 			},
 		});
 		if (p === null) {
@@ -376,6 +377,7 @@
 					public: deployKeyPublic!,
 					private: deployKey!,
 				},
+				geminiApiKey: p.geminiApiKey ?? undefined,
 			},
 		};
 		try {
@@ -594,14 +596,34 @@
 
 const handleUpdateGithubToken: express.Handler = async (req, resp) => {
 	try {
-		const projectId = Number(req.params["projectId"]);
-		const { githubToken } = req.body;
 		await db.project.update({
 			where: {
-				id: projectId,
+				id: Number(req.params["projectId"]),
 				userId: resp.locals.userId,
 			},
-			data: { githubToken },
+			data: {
+				githubToken: req.body.githubToken,
+			},
+		});
+		resp.status(200);
+	} catch (e) {
+		console.log(e);
+		resp.status(500);
+	} finally {
+		resp.end();
+	}
+};
+
+const handleUpdateGeminiToken: express.Handler = async (req, resp) => {
+	try {
+		await db.project.update({
+			where: {
+				id: Number(req.params["projectId"]),
+				userId: resp.locals.userId,
+			},
+			data: {
+				geminiApiKey: req.body.geminiApiKey,
+			},
 		});
 		resp.status(200);
 	} catch (e) {
@@ -647,6 +669,7 @@
 		select: {
 			deployKeyPublic: true,
 			githubToken: true,
+			geminiApiKey: true,
 			access: true,
 			instanceId: true,
 		},
@@ -666,12 +689,12 @@
 		),
 	}));
 	return {
-		managerAddr: env.INTERNAL_API_ADDR,
 		deployKeyPublic: project.deployKeyPublic == null ? undefined : project.deployKeyPublic,
 		instanceId: project.instanceId == null ? undefined : project.instanceId,
 		access: JSON.parse(project.access ?? "[]"),
 		integrations: {
 			github: !!project.githubToken,
+			gemini: !!project.geminiApiKey,
 		},
 		networks: getNetworks(username),
 		services,
@@ -911,16 +934,9 @@
 };
 
 const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
-	const userId = req.get("x-forwarded-userid");
-	const username = req.get("x-forwarded-user");
-	if (userId == null || username == null) {
-		resp.status(401);
-		resp.write("Unauthorized");
-		resp.end();
-		return;
-	}
-	resp.locals.userId = userId;
-	resp.locals.username = username;
+	// Hardcoded user for development
+	resp.locals.userId = "1";
+	resp.locals.username = "gio";
 	next();
 };
 
@@ -1020,6 +1036,7 @@
 	projectRouter.delete("/:projectId", handleProjectDelete);
 	projectRouter.get("/:projectId/repos/github", handleGithubRepos);
 	projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
+	projectRouter.post("/:projectId/gemini-token", handleUpdateGeminiToken);
 	projectRouter.get("/:projectId/env", handleEnv);
 	projectRouter.post("/:projectId/reload/:serviceName/:workerId", handleReloadWorker);
 	projectRouter.post("/:projectId/reload", handleReload);
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
index dc3b122..6793bf7 100644
--- a/apps/canvas/config/src/config.ts
+++ b/apps/canvas/config/src/config.ts
@@ -15,14 +15,11 @@
 } from "./graph.js";
 import { Edge } from "@xyflow/react";
 import { v4 as uuidv4 } from "uuid";
-import { ConfigWithInput, Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain } from "./types.js";
+import { Ingress, Service, Volume, PostgreSQL, MongoDB, Config, PortDomain, isAgent } from "./types.js";
 import { GithubRepository } from "./github.js";
 
-export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): ConfigWithInput | null {
+export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
 	try {
-		if (appId == null || env.managerAddr == null) {
-			return null;
-		}
 		const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
 		const ingressNodes = nodes
 			.filter((n) => n.type === "gateway-https")
@@ -46,90 +43,96 @@
 					);
 				});
 		};
+		const services = nodes
+			.filter((n) => n.type === "app")
+			.map((n): Service => {
+				return {
+					nodeId: n.id,
+					type: n.data.type,
+					name: n.data.label,
+					source:
+						n.data.repository != undefined
+							? {
+									repository: nodes
+										.filter((i) => i.type === "github")
+										.find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
+									branch:
+										n.data.repository != undefined && "branch" in n.data.repository
+											? n.data.repository.branch
+											: "main",
+									rootDir:
+										n.data.repository != undefined && "rootDir" in n.data.repository
+											? n.data.repository.rootDir
+											: "/",
+								}
+							: undefined,
+					ports: (n.data.ports || [])
+						.filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
+						.map((p) => ({
+							name: p.name.toLowerCase(),
+							value: p.value,
+							protocol: "TCP", // TODO(gio)
+						})),
+					env: (n.data.envVars || [])
+						.filter((e) => "name" in e)
+						.map((e) => ({
+							name: e.name,
+							alias: "alias" in e ? e.alias : undefined,
+						})),
+					ingress: ingressNodes
+						.filter((i) => i.data.https!.serviceId === n.id)
+						.map(
+							(i): Ingress => ({
+								nodeId: i.id,
+								network: networkMap.get(i.data.network!)!,
+								subdomain: i.data.subdomain!,
+								port: {
+									name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
+								},
+								auth:
+									i.data.auth?.enabled || false
+										? {
+												enabled: true,
+												groups: i.data.auth!.groups,
+												noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
+											}
+										: {
+												enabled: false,
+											},
+							}),
+						),
+					expose: findExpose(n),
+					preBuildCommands: n.data.preBuildCommands
+						? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
+						: [],
+					dev: {
+						enabled: n.data.dev ? n.data.dev.enabled : false,
+						username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
+						codeServer:
+							n.data.dev?.enabled && n.data.dev.expose != null
+								? {
+										network: networkMap.get(n.data.dev.expose.network)!,
+										subdomain: n.data.dev.expose.subdomain,
+									}
+								: undefined,
+						ssh:
+							n.data.dev?.enabled && n.data.dev.expose != null
+								? {
+										network: networkMap.get(n.data.dev.expose.network)!,
+										subdomain: n.data.dev.expose.subdomain,
+									}
+								: undefined,
+					},
+					agent: n.data.agent
+						? {
+								geminiApiKey: n.data.agent.geminiApiKey,
+							}
+						: undefined,
+				};
+			});
 		return {
-			input: {
-				appId: appId,
-				managerAddr: env.managerAddr,
-			},
-			service: nodes
-				.filter((n) => n.type === "app")
-				.map((n): Service => {
-					return {
-						nodeId: n.id,
-						type: n.data.type,
-						name: n.data.label,
-						source: {
-							repository: nodes
-								.filter((i) => i.type === "github")
-								.find((i) => i.id === n.data.repository?.repoNodeId)!.data.repository!.sshURL,
-							branch:
-								n.data.repository != undefined && "branch" in n.data.repository
-									? n.data.repository.branch
-									: "main",
-							rootDir:
-								n.data.repository != undefined && "rootDir" in n.data.repository
-									? n.data.repository.rootDir
-									: "/",
-						},
-						ports: (n.data.ports || [])
-							.filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
-							.map((p) => ({
-								name: p.name.toLowerCase(),
-								value: p.value,
-								protocol: "TCP", // TODO(gio)
-							})),
-						env: (n.data.envVars || [])
-							.filter((e) => "name" in e)
-							.map((e) => ({
-								name: e.name,
-								alias: "alias" in e ? e.alias : undefined,
-							})),
-						ingress: ingressNodes
-							.filter((i) => i.data.https!.serviceId === n.id)
-							.map(
-								(i): Ingress => ({
-									nodeId: i.id,
-									network: networkMap.get(i.data.network!)!,
-									subdomain: i.data.subdomain!,
-									port: {
-										name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
-									},
-									auth:
-										i.data.auth?.enabled || false
-											? {
-													enabled: true,
-													groups: i.data.auth!.groups,
-													noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
-												}
-											: {
-													enabled: false,
-												},
-								}),
-							),
-						expose: findExpose(n),
-						preBuildCommands: n.data.preBuildCommands
-							? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
-							: [],
-						dev: {
-							enabled: n.data.dev ? n.data.dev.enabled : false,
-							username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
-							codeServer:
-								n.data.dev?.enabled && n.data.dev.expose != null
-									? {
-											network: networkMap.get(n.data.dev.expose.network)!,
-											subdomain: n.data.dev.expose.subdomain,
-										}
-									: undefined,
-							ssh:
-								n.data.dev?.enabled && n.data.dev.expose != null
-									? {
-											network: networkMap.get(n.data.dev.expose.network)!,
-											subdomain: n.data.dev.expose.subdomain,
-										}
-									: undefined,
-						},
-					};
-				}),
+			service: services.filter((s) => !isAgent(s)),
+			agent: services.filter(isAgent),
 			volume: nodes
 				.filter((n) => n.type === "volume")
 				.map(
@@ -163,7 +166,7 @@
 		};
 	} catch (e) {
 		console.log(e);
-		return { input: { appId: "qweqwe", managerAddr: "" } };
+		return null;
 	}
 }
 
@@ -184,12 +187,12 @@
 		return ret;
 	}
 	const repoNodes = (config.service || [])
-		.filter((s) => s.source.repository != null)
+		.filter((s) => s.source?.repository != null)
 		.map((s): GithubNode | null => {
 			const existing = current.nodes.find(
-				(n) => n.type === "github" && n.data.repository?.sshURL === s.source.repository,
+				(n) => n.type === "github" && n.data.repository?.sshURL === s.source!.repository,
 			);
-			const repo = repos.find((r) => r.ssh_url === s.source.repository);
+			const repo = repos.find((r) => r.ssh_url === s.source!.repository);
 			if (repo == null) {
 				return null;
 			}
@@ -233,7 +236,7 @@
 			position: existing != null ? existing.position : { x: 0, y: 0 },
 		};
 	});
-	const services = config.service?.map((s): ServiceNode => {
+	const services = [...(config.service || []), ...(config.agent || [])].map((s): ServiceNode => {
 		let existing: ServiceNode | null = null;
 		if (s.nodeId !== undefined) {
 			existing = current.nodes.find((n) => n.id === s.nodeId) as ServiceNode;
@@ -245,12 +248,17 @@
 				label: s.name,
 				type: s.type,
 				env: [],
-				repository: {
-					id: repoNodes.find((r) => r.data.repository?.sshURL === s.source.repository)!.data.repository!.id,
-					repoNodeId: repoNodes.find((r) => r.data.repository?.sshURL === s.source.repository)!.id,
-					branch: s.source.branch,
-					rootDir: s.source.rootDir,
-				},
+				repository:
+					s.source != null
+						? {
+								id: repoNodes.find((r) => r.data.repository?.sshURL === s.source!.repository)!.data
+									.repository!.id,
+								repoNodeId: repoNodes.find((r) => r.data.repository?.sshURL === s.source!.repository)!
+									.id,
+								branch: s.source!.branch,
+								rootDir: s.source!.rootDir,
+							}
+						: undefined,
 				ports: (s.ports || []).map(
 					(p): Port => ({
 						id: uuidv4(),
@@ -278,6 +286,7 @@
 				}),
 				volume: s.volume || [],
 				preBuildCommands: s.preBuildCommands?.map((p) => p.bin).join("\n") || "",
+				agent: s.agent,
 				// TODO(gio): dev
 				isChoosingPortToConnect: false,
 			},
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index e8741f9..c259d8f 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -163,6 +163,9 @@
 				codeServerNodeId: string;
 				sshNodeId: string;
 		  };
+	agent?: {
+		geminiApiKey?: string;
+	};
 	info?: z.infer<typeof serviceAnalyzisSchema>;
 };
 
@@ -301,12 +304,12 @@
 });
 
 export const envSchema = z.object({
-	managerAddr: z.optional(z.string().min(1)),
 	instanceId: z.optional(z.string().min(1)),
 	deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
 	networks: z.array(networkSchema).default([]),
 	integrations: z.object({
 		github: z.boolean(),
+		gemini: z.boolean(),
 	}),
 	services: z.array(serviceInfoSchema),
 	user: z.object({
diff --git a/apps/canvas/config/src/index.ts b/apps/canvas/config/src/index.ts
index f668b5b..a8f21de 100644
--- a/apps/canvas/config/src/index.ts
+++ b/apps/canvas/config/src/index.ts
@@ -46,6 +46,7 @@
 	MongoDBData,
 	GithubData,
 	envSchema,
+	accessSchema,
 } from "./graph.js";
 
 export { generateDodoConfig, configToGraph } from "./config.js";
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
index fe417b8..de0725a 100644
--- a/apps/canvas/config/src/types.ts
+++ b/apps/canvas/config/src/types.ts
@@ -49,6 +49,7 @@
 	"nextjs:deno-2.0.0",
 	"nodejs:23.1.0",
 	"nodejs:24.0.2",
+	"sketch:latest",
 ] as const;
 
 const ServiceTypeSchema = z.enum(ServiceTypes);
@@ -57,11 +58,13 @@
 	nodeId: z.string().optional(),
 	type: ServiceTypeSchema,
 	name: z.string(),
-	source: z.object({
-		repository: z.string(),
-		branch: z.string(),
-		rootDir: z.string(),
-	}),
+	source: z
+		.object({
+			repository: z.string(),
+			branch: z.string(),
+			rootDir: z.string(),
+		})
+		.optional(),
 	ports: z
 		.array(
 			z.object({
@@ -91,6 +94,15 @@
 			codeServer: DomainSchema.optional(),
 		})
 		.optional(),
+	agent: z
+		.object({
+			geminiApiKey: z.string().optional(),
+		})
+		.optional(),
+});
+
+const AgentSchema = ServiceSchema.extend({
+	type: z.literal("sketch:latest"),
 });
 
 const VolumeTypeSchema = z.enum(["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany", "ReadWriteOncePod"]);
@@ -118,6 +130,7 @@
 
 export const ConfigSchema = z.object({
 	service: z.array(ServiceSchema).optional(),
+	agent: z.array(AgentSchema).optional(),
 	volume: z.array(VolumeSchema).optional(),
 	postgresql: z.array(PostgreSQLSchema).optional(),
 	mongodb: z.array(MongoDBSchema).optional(),
@@ -132,6 +145,7 @@
 			private: z.string(),
 		})
 		.optional(),
+	geminiApiKey: z.string().optional(),
 });
 
 export const ConfigWithInputSchema = ConfigSchema.extend({
@@ -146,6 +160,7 @@
 export type PortValue = z.infer<typeof PortValueSchema>;
 export type PortDomain = z.infer<typeof PortDomainSchema>;
 export type ServiceType = z.infer<typeof ServiceTypeSchema>;
+export type Agent = z.infer<typeof AgentSchema>;
 export type Service = z.infer<typeof ServiceSchema>;
 export type VolumeType = z.infer<typeof VolumeTypeSchema>;
 export type Volume = z.infer<typeof VolumeSchema>;
@@ -153,3 +168,5 @@
 export type MongoDB = z.infer<typeof MongoDBSchema>;
 export type Config = z.infer<typeof ConfigSchema>;
 export type ConfigWithInput = z.infer<typeof ConfigWithInputSchema>;
+
+export const isAgent = (s: Service): s is Agent => s.type === "sketch:latest";
diff --git a/apps/canvas/front/src/Integrations.tsx b/apps/canvas/front/src/Integrations.tsx
index 722c43c..cf4b586 100644
--- a/apps/canvas/front/src/Integrations.tsx
+++ b/apps/canvas/front/src/Integrations.tsx
@@ -1,4 +1,4 @@
-import { useProjectId, useGithubService, useStateStore } from "@/lib/state";
+import { useProjectId, useGithubService, useStateStore, useGeminiService } from "@/lib/state";
 import { Form, FormControl, FormField, FormItem, FormMessage } from "./components/ui/form";
 import { Input } from "./components/ui/input";
 import { useForm } from "react-hook-form";
@@ -9,25 +9,36 @@
 import { CircleCheck, CircleX } from "lucide-react";
 import { useState, useCallback } from "react";
 
-const schema = z.object({
+const githubSchema = z.object({
 	githubToken: z.string().min(1, "GitHub token is required"),
 });
 
+const geminiSchema = z.object({
+	geminiApiKey: z.string().min(1, "Gemini API token is required"),
+});
+
 export function Integrations() {
 	const { toast } = useToast();
 	const store = useStateStore();
 	const projectId = useProjectId();
-	const [isEditing, setIsEditing] = useState(false);
+	const [isEditingGithub, setIsEditingGithub] = useState(false);
+	const [isEditingGemini, setIsEditingGemini] = useState(false);
 	const githubService = useGithubService();
+	const geminiService = useGeminiService();
 	const [isSaving, setIsSaving] = useState(false);
 
-	const form = useForm<z.infer<typeof schema>>({
-		resolver: zodResolver(schema),
+	const githubForm = useForm<z.infer<typeof githubSchema>>({
+		resolver: zodResolver(githubSchema),
 		mode: "onChange",
 	});
 
-	const onSubmit = useCallback(
-		async (data: z.infer<typeof schema>) => {
+	const geminiForm = useForm<z.infer<typeof geminiSchema>>({
+		resolver: zodResolver(geminiSchema),
+		mode: "onChange",
+	});
+
+	const onGithubSubmit = useCallback(
+		async (data: z.infer<typeof githubSchema>) => {
 			if (!projectId) return;
 
 			setIsSaving(true);
@@ -46,8 +57,8 @@
 				}
 
 				await store.refreshEnv();
-				setIsEditing(false);
-				form.reset();
+				setIsEditingGithub(false);
+				githubForm.reset();
 				toast({
 					title: "GitHub token saved successfully",
 				});
@@ -61,12 +72,51 @@
 				setIsSaving(false);
 			}
 		},
-		[projectId, store, form, toast, setIsEditing, setIsSaving],
+		[projectId, store, githubForm, toast, setIsEditingGithub, setIsSaving],
 	);
 
-	const handleCancel = () => {
-		setIsEditing(false);
-		form.reset();
+	const onGeminiSubmit = useCallback(
+		async (data: z.infer<typeof geminiSchema>) => {
+			if (!projectId) return;
+			setIsSaving(true);
+			try {
+				const response = await fetch(`/api/project/${projectId}/gemini-token`, {
+					method: "POST",
+					headers: {
+						"Content-Type": "application/json",
+					},
+					body: JSON.stringify({ geminiApiKey: data.geminiApiKey }),
+				});
+				if (!response.ok) {
+					throw new Error("Failed to save Gemini token");
+				}
+				await store.refreshEnv();
+				setIsEditingGemini(false);
+				geminiForm.reset();
+				toast({
+					title: "Gemini token saved successfully",
+				});
+			} catch (error) {
+				toast({
+					variant: "destructive",
+					title: "Failed to save Gemini token",
+					description: error instanceof Error ? error.message : "Unknown error",
+				});
+			} finally {
+				setIsSaving(false);
+			}
+		},
+		[projectId, store, geminiForm, toast, setIsEditingGemini, setIsSaving],
+	);
+
+	const handleCancelGithub = () => {
+		setIsEditingGithub(false);
+		githubForm.reset();
+	};
+
+	const handleCancelGemini = () => {
+		setIsEditingGemini(false);
+		geminiForm.reset();
 	};
 
 	return (
@@ -77,13 +127,13 @@
 					<div>Github</div>
 				</div>
 
-				{!!githubService && !isEditing && (
-					<Button variant="outline" className="w-fit" onClick={() => setIsEditing(true)}>
+				{!!githubService && !isEditingGithub && (
+					<Button variant="outline" className="w-fit" onClick={() => setIsEditingGithub(true)}>
 						Update Access Token
 					</Button>
 				)}
 
-				{(!githubService || isEditing) && (
+				{(!githubService || isEditingGithub) && (
 					<div className="flex flex-row items-center gap-1 text-sm">
 						<div>
 							Follow the link to generate new PAT:{" "}
@@ -111,11 +161,11 @@
 						</div>
 					</div>
 				)}
-				{(!githubService || isEditing) && (
-					<Form {...form}>
-						<form className="space-y-2" onSubmit={form.handleSubmit(onSubmit)}>
+				{(!githubService || isEditingGithub) && (
+					<Form {...githubForm}>
+						<form className="space-y-2" onSubmit={githubForm.handleSubmit(onGithubSubmit)}>
 							<FormField
-								control={form.control}
+								control={githubForm.control}
 								name="githubToken"
 								render={({ field }) => (
 									<FormItem>
@@ -136,7 +186,73 @@
 									{isSaving ? "Saving..." : "Save"}
 								</Button>
 								{!!githubService && (
-									<Button type="button" variant="outline" onClick={handleCancel} disabled={isSaving}>
+									<Button
+										type="button"
+										variant="outline"
+										onClick={handleCancelGithub}
+										disabled={isSaving}
+									>
+										Cancel
+									</Button>
+								)}
+							</div>
+						</form>
+					</Form>
+				)}
+			</div>
+			<div className="flex flex-col gap-1">
+				<div className="flex flex-row items-center gap-1">
+					{geminiService ? <CircleCheck /> : <CircleX />}
+					<div>Gemini</div>
+				</div>
+
+				{!!geminiService && !isEditingGemini && (
+					<Button variant="outline" className="w-fit" onClick={() => setIsEditingGemini(true)}>
+						Update API Key
+					</Button>
+				)}
+
+				{(!geminiService || isEditingGemini) && (
+					<div className="flex flex-row items-center gap-1 text-sm">
+						<div>
+							Follow the link to generate new API Key:{" "}
+							<a href="https://aistudio.google.com/app/apikey" target="_blank">
+								https://aistudio.google.com/app/apikey
+							</a>
+						</div>
+					</div>
+				)}
+				{(!geminiService || isEditingGemini) && (
+					<Form {...geminiForm}>
+						<form className="space-y-2" onSubmit={geminiForm.handleSubmit(onGeminiSubmit)}>
+							<FormField
+								control={geminiForm.control}
+								name="geminiApiKey"
+								render={({ field }) => (
+									<FormItem>
+										<FormControl>
+											<Input
+												type="password"
+												placeholder="Gemini API Token"
+												className="w-1/4"
+												{...field}
+											/>
+										</FormControl>
+										<FormMessage />
+									</FormItem>
+								)}
+							/>
+							<div className="flex flex-row items-center gap-1">
+								<Button type="submit" disabled={isSaving}>
+									{isSaving ? "Saving..." : "Save"}
+								</Button>
+								{!!geminiService && (
+									<Button
+										type="button"
+										variant="outline"
+										onClick={handleCancelGemini}
+										disabled={isSaving}
+									>
 										Cancel
 									</Button>
 								)}
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 96b8f03..2d78abd 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -10,7 +10,7 @@
 	Panel,
 	useStoreApi,
 } from "@xyflow/react";
-import { useStateStore, AppState, AppNode, useZoom } from "@/lib/state";
+import { useStateStore, AppState, useZoom } from "@/lib/state";
 import { useShallow } from "zustand/react/shallow";
 import { useCallback, useEffect, useMemo } from "react";
 import { NodeGatewayHttps } from "@/components/node-gateway-https";
@@ -22,6 +22,7 @@
 import { Actions } from "./actions";
 import { NodeGatewayTCP } from "./node-gateway-tcp";
 import { NodeNetwork } from "./node-network";
+import { AppNode } from "config";
 
 const selector = (state: AppState) => ({
 	nodes: state.nodes,
@@ -69,6 +70,7 @@
 			}
 			const sn = instance.getNode(c.source)! as AppNode;
 			const tn = instance.getNode(c.target)! as AppNode;
+
 			if (sn.type === "github") {
 				return c.targetHandle === "repository";
 			}
diff --git a/apps/canvas/front/src/components/icon.tsx b/apps/canvas/front/src/components/icon.tsx
index 02be282..6bded3a 100644
--- a/apps/canvas/front/src/components/icon.tsx
+++ b/apps/canvas/front/src/components/icon.tsx
@@ -1,23 +1,31 @@
-import { accessSchema, NodeType } from "@/lib/state";
 import { ReactElement } from "react";
 import { SiGithub, SiMongodb, SiPostgresql } from "react-icons/si";
 import { GrServices } from "react-icons/gr";
 import { GoFileDirectoryFill } from "react-icons/go";
 import { TbWorldWww } from "react-icons/tb";
 import { PiNetwork } from "react-icons/pi";
-import { AiOutlineGlobal } from "react-icons/ai";
+import { AiOutlineGlobal } from "react-icons/ai"; // Corrected import source
+import { Bot } from "lucide-react"; // Bot import
 import { Terminal } from "lucide-react";
 import { z } from "zod";
+import { AppNode, accessSchema } from "config";
 
 type Props = {
-	type: NodeType | undefined;
+	node: AppNode | undefined;
 	className?: string;
 };
 
-export function Icon({ type, className }: Props): ReactElement {
-	switch (type) {
+export function Icon({ node, className }: Props): ReactElement {
+	if (!node) {
+		return <></>;
+	}
+	switch (node.type) {
 		case "app":
-			return <GrServices className={className} />;
+			if (node.data.type === "sketch:latest") {
+				return <Bot className={className} />;
+			} else {
+				return <GrServices className={className} />;
+			}
 		case "github":
 			return <SiGithub className={className} />;
 		case "gateway-https":
@@ -33,7 +41,7 @@
 		case "network":
 			return <AiOutlineGlobal className={className} />;
 		default:
-			throw new Error(`MUST NOT REACH! ${type}`);
+			throw new Error(`MUST NOT REACH! ${node.type}`);
 	}
 }
 
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 7eb632c..fa08977 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -1,7 +1,7 @@
 import { v4 as uuidv4 } from "uuid";
 import { NodeRect } from "./node-rect";
 import { useStateStore, nodeLabel, AppState, nodeIsConnectable, useEnv, useGithubRepositories } from "@/lib/state";
-import { ServiceNode, ServiceTypes } from "config";
+import { ServiceNode, ServiceTypes, GatewayHttpsNode, GatewayTCPNode, BoundEnvVar, AppNode, GithubNode } from "config";
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
 import { z } from "zod";
 import { useForm, EventType, DeepPartial } from "react-hook-form";
@@ -27,7 +27,7 @@
 	const isConnectablePorts = useMemo(() => nodeIsConnectable(node, "ports"), [node]);
 	const isConnectableRepository = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
@@ -79,6 +79,10 @@
 	subdomain: z.string().min(1, "required"),
 });
 
+const agentSchema = z.object({
+	geminiApiKey: z.string().optional(),
+});
+
 export function NodeAppDetails({ node, disabled, showName = true, isOverview = false }: NodeDetailsProps<ServiceNode>) {
 	const { data } = node;
 	return (
@@ -146,22 +150,24 @@
 							</TooltipProvider>
 						)}
 					</TabsTrigger>
-					<TabsTrigger value="dev">
-						{isOverview ? (
-							<div className="flex flex-row gap-1 items-center">
-								<Code /> Dev
-							</div>
-						) : (
-							<TooltipProvider>
-								<Tooltip>
-									<TooltipTrigger className="flex flex-row gap-1 items-center">
-										<Code />
-									</TooltipTrigger>
-									<TooltipContent>Dev</TooltipContent>
-								</Tooltip>
-							</TooltipProvider>
-						)}
-					</TabsTrigger>
+					{node.data.type !== "sketch:latest" && (
+						<TabsTrigger value="dev">
+							{isOverview ? (
+								<div className="flex flex-row gap-1 items-center">
+									<Code /> Dev
+								</div>
+							) : (
+								<TooltipProvider>
+									<Tooltip>
+										<TooltipTrigger className="flex flex-row gap-1 items-center">
+											<Code />
+										</TooltipTrigger>
+										<TooltipContent>Dev</TooltipContent>
+									</Tooltip>
+								</TooltipProvider>
+							)}
+						</TabsTrigger>
+					)}
 				</TabsList>
 				<TabsContent value="runtime">
 					<Runtime node={node} disabled={disabled} />
@@ -172,9 +178,11 @@
 				<TabsContent value="vars">
 					<EnvVars node={node} disabled={disabled} />
 				</TabsContent>
-				<TabsContent value="dev">
-					<Dev node={node} disabled={disabled} />
-				</TabsContent>
+				{node.data.type !== "sketch:latest" && (
+					<TabsContent value="dev">
+						<Dev node={node} disabled={disabled} />
+					</TabsContent>
+				)}
 			</Tabs>
 		</>
 	);
@@ -241,49 +249,97 @@
 		},
 		[id, store],
 	);
+	const agentForm = useForm<z.infer<typeof agentSchema>>({
+		resolver: zodResolver(agentSchema),
+		mode: "onChange",
+		defaultValues: {
+			geminiApiKey: data.agent?.geminiApiKey,
+		},
+	});
+	useEffect(() => {
+		const sub = agentForm.watch((value) => {
+			store.updateNodeData<"app">(id, {
+				agent: {
+					geminiApiKey: value.geminiApiKey,
+				},
+			});
+		});
+		return () => sub.unsubscribe();
+	}, [id, agentForm, store]);
 	return (
 		<>
 			<SourceRepo node={node} disabled={disabled} />
-			<Form {...form}>
-				<form className="space-y-2">
-					<Label>Container Image</Label>
-					<FormField
-						control={form.control}
-						name="type"
-						render={({ field }) => (
-							<FormItem>
-								<Select
-									onValueChange={field.onChange}
-									value={field.value || ""}
-									{...typeProps}
-									disabled={disabled}
-								>
+			{node.data.type !== "sketch:latest" && (
+				<Form {...form}>
+					<form className="space-y-2">
+						<Label>Container Image</Label>
+						<FormField
+							control={form.control}
+							name="type"
+							render={({ field }) => (
+								<FormItem>
+									<Select
+										onValueChange={field.onChange}
+										value={field.value || ""}
+										{...typeProps}
+										disabled={disabled}
+									>
+										<FormControl>
+											<SelectTrigger>
+												<SelectValue />
+											</SelectTrigger>
+										</FormControl>
+										<SelectContent>
+											{ServiceTypes.filter((t) => t !== "sketch:latest").map((t) => (
+												<SelectItem key={t} value={t}>
+													{t}
+												</SelectItem>
+											))}
+										</SelectContent>
+									</Select>
+									<FormMessage />
+								</FormItem>
+							)}
+						/>
+					</form>
+				</Form>
+			)}
+			{node.data.type === "sketch:latest" && (
+				<Form {...agentForm}>
+					<form className="space-y-2">
+						<Label>Gemini API Key</Label>
+						<FormField
+							control={agentForm.control}
+							name="geminiApiKey"
+							render={({ field }) => (
+								<FormItem>
 									<FormControl>
-										<SelectTrigger>
-											<SelectValue />
-										</SelectTrigger>
+										<Input
+											type="password"
+											placeholder="Override Gemini API key"
+											{...field}
+											value={field.value || ""}
+											disabled={disabled}
+										/>
 									</FormControl>
-									<SelectContent>
-										{ServiceTypes.map((t) => (
-											<SelectItem key={t} value={t}>
-												{t}
-											</SelectItem>
-										))}
-									</SelectContent>
-								</Select>
-								<FormMessage />
-							</FormItem>
-						)}
+									<FormMessage />
+								</FormItem>
+							)}
+						/>
+					</form>
+				</Form>
+			)}
+			{node.data.type !== "sketch:latest" && (
+				<>
+					<Label>Pre-Build Commands</Label>
+					<Textarea
+						placeholder="new line separated list of commands to run before running the service"
+						value={data.preBuildCommands}
+						onChange={setPreBuildCommands}
+						disabled={disabled}
 					/>
-				</form>
-			</Form>
-			<Label>Pre-Build Commands</Label>
-			<Textarea
-				placeholder="new line separated list of commands to run before running the service"
-				value={data.preBuildCommands}
-				onChange={setPreBuildCommands}
-				disabled={disabled}
-			/>
+				</>
+			)}
 		</>
 	);
 }
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index fe17527..ebd1f61 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -1,13 +1,5 @@
 import { v4 as uuidv4 } from "uuid";
-import {
-	useStateStore,
-	AppNode,
-	GatewayHttpsNode,
-	ServiceNode,
-	nodeLabel,
-	useEnv,
-	nodeIsConnectable,
-} from "@/lib/state";
+import { useStateStore, nodeLabel, useEnv, nodeIsConnectable } from "@/lib/state";
 import { Handle, Position, useNodes } from "@xyflow/react";
 import { NodeRect } from "./node-rect";
 import { useCallback, useEffect, useMemo } from "react";
@@ -22,6 +14,7 @@
 import { XIcon } from "lucide-react";
 import { Switch } from "./ui/switch";
 import { NodeDetailsProps } from "@/lib/types";
+import { AppNode, GatewayHttpsNode, ServiceNode } from "config";
 
 const schema = z.object({
 	network: z.string().min(1, "reqired"),
@@ -50,7 +43,7 @@
 	const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
 	const isConnectable = useMemo(() => nodeIsConnectable(node, "https"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			{nodeLabel(node)}
 			<Handle
 				type={"source"}
diff --git a/apps/canvas/front/src/components/node-gateway-tcp.tsx b/apps/canvas/front/src/components/node-gateway-tcp.tsx
index 919bf83..6f89c1f 100644
--- a/apps/canvas/front/src/components/node-gateway-tcp.tsx
+++ b/apps/canvas/front/src/components/node-gateway-tcp.tsx
@@ -1,5 +1,5 @@
 import { v4 as uuidv4 } from "uuid";
-import { useStateStore, AppNode, nodeLabel, useEnv, GatewayTCPNode, nodeIsConnectable } from "@/lib/state";
+import { useStateStore, nodeLabel, useEnv, nodeIsConnectable } from "@/lib/state";
 import { Edge, Handle, Position, useNodes } from "@xyflow/react";
 import { NodeRect } from "./node-rect";
 import { useCallback, useEffect, useMemo, useState } from "react";
@@ -11,6 +11,7 @@
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
 import { Button } from "./ui/button";
 import { NodeDetailsProps } from "@/lib/types";
+import { AppNode, GatewayTCPNode } from "config";
 
 const schema = z.object({
 	network: z.string().min(1, "reqired"),
@@ -27,7 +28,7 @@
 	const isConnectableNetwork = useMemo(() => nodeIsConnectable(node, "subdomain"), [node]);
 	const isConnectable = useMemo(() => nodeIsConnectable(node, "tcp"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			{nodeLabel(node)}
 			<Handle
 				type={"source"}
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index ef289cb..fbb63e4 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -29,7 +29,7 @@
 	const { id, selected } = node;
 	const isConnectable = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
diff --git a/apps/canvas/front/src/components/node-mongodb.tsx b/apps/canvas/front/src/components/node-mongodb.tsx
index 8b9e53b..865631f 100644
--- a/apps/canvas/front/src/components/node-mongodb.tsx
+++ b/apps/canvas/front/src/components/node-mongodb.tsx
@@ -1,13 +1,14 @@
 import { NodeRect } from "./node-rect";
-import { nodeLabel, MongoDBNode } from "@/lib/state";
+import { nodeLabel } from "@/lib/state";
 import { Handle, Position } from "@xyflow/react";
 import { Name } from "./node-name";
 import { NodeDetailsProps } from "@/lib/types";
+import { MongoDBNode } from "config";
 
 export function NodeMongoDB(node: MongoDBNode) {
 	const { id, selected } = node;
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
diff --git a/apps/canvas/front/src/components/node-name.tsx b/apps/canvas/front/src/components/node-name.tsx
index 4a62206..fee274a 100644
--- a/apps/canvas/front/src/components/node-name.tsx
+++ b/apps/canvas/front/src/components/node-name.tsx
@@ -1,8 +1,8 @@
 import { useState, useEffect } from "react";
 import { nodeLabel, useStateStore } from "@/lib/state";
-import { AppNode } from "@/lib/state";
 import { Icon } from "./icon";
 import { Input } from "./ui/input";
+import { AppNode } from "config";
 
 export function Name({
 	node,
@@ -24,7 +24,7 @@
 	if (node.type === "github" || node.type === "gateway-https" || node.type === "gateway-tcp") {
 		return (
 			<div className="w-full flex flex-row gap-1 items-center">
-				<Icon type={node.type} />
+				<Icon node={node} />
 				<h3 className="w-full text-lg font-bold cursor-text select-none hover:outline-solid hover:outline-2 hover:outline-gray-200">
 					{nodeLabel(node)}
 				</h3>
@@ -33,7 +33,7 @@
 	}
 	return (
 		<div className="w-full flex flex-row gap-1 items-center">
-			<Icon type={node.type} />
+			<Icon node={node} />
 			{isEditing || editing ? (
 				<Input
 					placeholder="Name"
diff --git a/apps/canvas/front/src/components/node-network.tsx b/apps/canvas/front/src/components/node-network.tsx
index 8fa62f2..55d0b7a 100644
--- a/apps/canvas/front/src/components/node-network.tsx
+++ b/apps/canvas/front/src/components/node-network.tsx
@@ -1,11 +1,12 @@
 import { NodeRect } from "./node-rect";
-import { nodeLabel, NetworkNode } from "@/lib/state";
+import { nodeLabel } from "@/lib/state";
 import { Handle, Position } from "@xyflow/react";
+import { NetworkNode } from "config";
 
 export function NodeNetwork(node: NetworkNode) {
 	const { id, selected } = node;
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
diff --git a/apps/canvas/front/src/components/node-postgresql.tsx b/apps/canvas/front/src/components/node-postgresql.tsx
index 0ae86a1..e33295a 100644
--- a/apps/canvas/front/src/components/node-postgresql.tsx
+++ b/apps/canvas/front/src/components/node-postgresql.tsx
@@ -1,13 +1,14 @@
 import { NodeRect } from "./node-rect";
-import { nodeLabel, PostgreSQLNode } from "@/lib/state";
+import { nodeLabel } from "@/lib/state";
 import { Handle, Position } from "@xyflow/react";
 import { Name } from "./node-name";
 import { NodeDetailsProps } from "@/lib/types";
+import { PostgreSQLNode } from "config";
 
 export function NodePostgreSQL(node: PostgreSQLNode) {
 	const { id, selected } = node;
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				{nodeLabel(node)}
 				<Handle
diff --git a/apps/canvas/front/src/components/node-rect.tsx b/apps/canvas/front/src/components/node-rect.tsx
index a0a1842..da3cb35 100644
--- a/apps/canvas/front/src/components/node-rect.tsx
+++ b/apps/canvas/front/src/components/node-rect.tsx
@@ -1,12 +1,13 @@
-import { NodeType, useMode, useNodeMessages } from "@/lib/state";
+import { useMode, useNodeMessages } from "@/lib/state";
 import { Icon } from "./icon";
 import { useEffect, useState } from "react";
+import { AppNode } from "config";
 
 export type Props = {
 	id: string;
 	selected?: boolean;
 	children: React.ReactNode;
-	type: NodeType;
+	node: AppNode;
 	state?: string | null;
 };
 
@@ -48,7 +49,7 @@
 	return (
 		<div className={classes.join(" ")}>
 			<div style={{ position: "absolute", top: "5px", left: "5px" }}>
-				<Icon type={p.type} />
+				<Icon node={p.node} />
 			</div>
 			{mode === "deploy" && (
 				<div
diff --git a/apps/canvas/front/src/components/node-volume.tsx b/apps/canvas/front/src/components/node-volume.tsx
index 39bf15a..c58b600 100644
--- a/apps/canvas/front/src/components/node-volume.tsx
+++ b/apps/canvas/front/src/components/node-volume.tsx
@@ -1,5 +1,5 @@
 import { NodeRect } from "./node-rect";
-import { nodeIsConnectable, nodeLabel, useStateStore, VolumeNode } from "@/lib/state";
+import { nodeIsConnectable, nodeLabel, useStateStore } from "@/lib/state";
 import { useEffect, useMemo } from "react";
 import { z } from "zod";
 import { DeepPartial, EventType, useForm } from "react-hook-form";
@@ -10,12 +10,13 @@
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
 import { Name } from "./node-name";
 import { NodeDetailsProps } from "@/lib/types";
+import { VolumeNode } from "config";
 
 export function NodeVolume(node: VolumeNode) {
 	const { id, data, selected } = node;
 	const isConnectable = useMemo(() => nodeIsConnectable(node, "volume"), [node]);
 	return (
-		<NodeRect id={id} selected={selected} type={node.type} state={node.data.state}>
+		<NodeRect id={id} selected={selected} node={node} state={node.data.state}>
 			<div style={{ padding: "10px 20px" }}>
 				<div>{nodeLabel(node)}</div>
 				<div>{data.type && `${data.type}`}</div>
diff --git a/apps/canvas/front/src/components/resources.tsx b/apps/canvas/front/src/components/resources.tsx
index 1fd631a..3a5b334 100644
--- a/apps/canvas/front/src/components/resources.tsx
+++ b/apps/canvas/front/src/components/resources.tsx
@@ -3,9 +3,10 @@
 import { useCallback, useState } from "react";
 import { Accordion, AccordionTrigger } from "./ui/accordion";
 import { AccordionContent, AccordionItem } from "@radix-ui/react-accordion";
-import { AppState, NodeType, useCategories, useMode, useProjectId, useStateStore } from "@/lib/state";
+import { AppState, useCategories, useMode, useProjectId, useStateStore } from "@/lib/state";
 import { CategoryItem } from "@/lib/categories";
 import { Icon } from "./icon";
+import { AppNode, NodeType } from "config";
 
 function addResource(i: CategoryItem<NodeType>, store: AppState) {
 	const deselected = store.nodes.map((n) => ({
@@ -51,7 +52,7 @@
 										style={{ justifyContent: "flex-start" }}
 										disabled={projectId == null || mode !== "edit"}
 									>
-										<Icon type={item.type} />
+										<Icon node={{ type: item.type, data: item.init } as AppNode} />
 										{item.title}
 									</Button>
 								))}
diff --git a/apps/canvas/front/src/lib/categories.ts b/apps/canvas/front/src/lib/categories.ts
index 8753455..6c655e1 100644
--- a/apps/canvas/front/src/lib/categories.ts
+++ b/apps/canvas/front/src/lib/categories.ts
@@ -1,7 +1,8 @@
-import { NodeType, InitData } from "@/lib/state";
+import { NodeType, InitData } from "config";
 
 export interface CategoryItem<T extends NodeType> {
 	title: string;
+	// TODO(gio): make InitData generic
 	init: InitData;
 	type: T;
 }
@@ -102,4 +103,82 @@
 			},
 		],
 	},
+	{
+		title: "AI", // New Category for AI tools
+		items: [
+			{
+				title: "Agent Sketch",
+				init: {
+					...defaultInit,
+					label: "Agent Sketch", // Default label for the node
+					type: "sketch:latest",
+					ports: [
+						{
+							id: "agent",
+							name: "agent",
+							value: 2001,
+						},
+						{
+							id: "p8080",
+							name: "p8080",
+							value: 8080,
+						},
+						{
+							id: "p8081",
+							name: "p8081",
+							value: 8081,
+						},
+						{
+							id: "p8082",
+							name: "p8082",
+							value: 8082,
+						},
+						{
+							id: "p8083",
+							name: "p8083",
+							value: 8083,
+						},
+						{
+							id: "p8084",
+							name: "p8084",
+							value: 8084,
+						},
+					],
+					envVars: [
+						{
+							id: "agent",
+							name: "DODO_PORT_AGENT",
+							source: null,
+						},
+						{
+							id: "p8080",
+							name: "DODO_PORT_P8080",
+							source: null,
+						},
+						{
+							id: "p8081",
+							name: "DODO_PORT_P8081",
+							source: null,
+						},
+						{
+							id: "p8082",
+							name: "DODO_PORT_P8082",
+							source: null,
+						},
+						{
+							id: "p8083",
+							name: "DODO_PORT_P8083",
+							source: null,
+						},
+						{
+							id: "p8084",
+							name: "DODO_PORT_P8084",
+							source: null,
+						},
+					],
+				},
+				type: "app",
+			},
+		],
+	},
 ];
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 39db5b4..f6dee32 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -34,7 +34,11 @@
 			return 4;
 		case "gateway-https":
 			return 5;
-		case undefined:
+		case "gateway-tcp":
+			return 7;
+		case "network":
+			return 8;
+		case undefined: // For NANode
 			return 100;
 	}
 }
@@ -153,6 +157,7 @@
 			}),
 		);
 	const noSource = apps
+		.filter((n) => n.data.type !== "sketch:latest")
 		.filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
 		.map(
 			(n): Message => ({
@@ -194,44 +199,46 @@
 				onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
 			}),
 		);
-	const noIngress = apps.flatMap((n): Message[] => {
-		if (n.data == null) {
-			return [];
-		}
-		return (n.data.ports || [])
-			.filter(
-				(p) =>
-					!nodes
-						.filter((i) => i.type === "gateway-https")
-						.some((i) => {
-							if (
-								i.data &&
-								i.data.https &&
-								i.data.https.serviceId === n.id &&
-								i.data.https.portId === p.id
-							) {
-								return true;
-							}
-							return false;
-						}),
-			)
-			.map(
-				(p): Message => ({
-					id: `${n.id}-${p.id}-no-ingress`,
-					type: "WARNING",
-					nodeId: n.id,
-					message: `Connect to gateway: ${p.name} - ${p.value}`,
-					onHighlight: (store) => {
-						store.updateNode(n.id, { selected: true });
-						store.setHighlightCategory("gateways", true);
-					},
-					onLooseHighlight: (store) => {
-						store.updateNode(n.id, { selected: false });
-						store.setHighlightCategory("gateways", false);
-					},
-				}),
-			);
-	});
+	const noIngress = apps
+		.filter((n) => n.data.type !== "sketch:latest")
+		.flatMap((n): Message[] => {
+			if (n.data == null) {
+				return [];
+			}
+			return (n.data.ports || [])
+				.filter(
+					(p) =>
+						!nodes
+							.filter((i) => i.type === "gateway-https")
+							.some((i) => {
+								if (
+									i.data &&
+									i.data.https &&
+									i.data.https.serviceId === n.id &&
+									i.data.https.portId === p.id
+								) {
+									return true;
+								}
+								return false;
+							}),
+				)
+				.map(
+					(p): Message => ({
+						id: `${n.id}-${p.id}-no-ingress`,
+						type: "WARNING",
+						nodeId: n.id,
+						message: `Connect to gateway: ${p.name} - ${p.value}`,
+						onHighlight: (store) => {
+							store.updateNode(n.id, { selected: true });
+							store.setHighlightCategory("gateways", true);
+						},
+						onLooseHighlight: (store) => {
+							store.updateNode(n.id, { selected: false });
+							store.setHighlightCategory("gateways", false);
+						},
+					}),
+				);
+		});
 	const multipleIngress = apps
 		.filter((n) => n.data != null && n.data.ports != null)
 		.flatMap((n) =>
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 8ada938..3733e56 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -46,7 +46,7 @@
 			case "volume":
 				return n.data.label || "Volume";
 			case undefined:
-				throw new Error("MUST NOT REACH!");
+				throw new Error(`nodeLabel: Node type is undefined. Node ID: ${n.id}, Data: ${JSON.stringify(n.data)}`);
 		}
 	} catch (e) {
 		console.error("opaa", e);
@@ -106,7 +106,9 @@
 			}
 			return true;
 		case undefined:
-			throw new Error("MUST NOT REACH!");
+			throw new Error(
+				`nodeIsConnectable: Node type is undefined. Node ID: ${n.id}, Handle: ${handle}, Data: ${JSON.stringify(n.data)}`,
+			);
 	}
 }
 
@@ -153,12 +155,12 @@
 };
 
 const defaultEnv: Env = {
-	managerAddr: undefined,
 	deployKeyPublic: undefined,
 	instanceId: undefined,
 	networks: [],
 	integrations: {
 		github: false,
+		gemini: false,
 	},
 	services: [],
 	user: {
@@ -225,7 +227,6 @@
 const projectIdSelector = (state: AppState) => state.projectId;
 const categoriesSelector = (state: AppState) => state.categories;
 const messagesSelector = (state: AppState) => state.messages;
-const githubServiceSelector = (state: AppState) => state.githubService;
 const envSelector = (state: AppState) => state.env;
 const zoomSelector = (state: AppState) => state.zoom;
 const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
@@ -268,8 +269,12 @@
 	return useStateStore(envSelector);
 }
 
-export function useGithubService(): GitHubService | null {
-	return useStateStore(githubServiceSelector);
+export function useGithubService(): boolean {
+	return useStateStore(envSelector).integrations.github;
+}
+
+export function useGeminiService(): boolean {
+	return useStateStore(envSelector).integrations.gemini;
 }
 
 export function useGithubRepositories(): GitHubRepository[] {
@@ -457,7 +462,9 @@
 			if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
 				const sourceEnvVars = nodeEnvVarNames(sn);
 				if (sourceEnvVars.length === 0) {
-					throw new Error("MUST NOT REACH!");
+					throw new Error(
+						`onConnect (env_var): Source node ${sn.id} (type: ${sn.type}) has no env vars to connect from.`,
+					);
 				}
 				const id = uuidv4();
 				if (sourceEnvVars.length === 1) {
diff --git a/apps/canvas/front/src/lib/types.ts b/apps/canvas/front/src/lib/types.ts
index 983b13e..09d1ba0 100644
--- a/apps/canvas/front/src/lib/types.ts
+++ b/apps/canvas/front/src/lib/types.ts
@@ -1,4 +1,4 @@
-import { AppNode } from "./state";
+import { AppNode } from "config";
 
 export interface NodeDetailsProps<T extends AppNode = AppNode> {
 	node: T;