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/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);