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