Compare commits
1 Commits
08e6f66c7d
...
8382f6645e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8382f6645e |
6
drizzle/migrations/0002_next_xorn.sql
Normal file
6
drizzle/migrations/0002_next_xorn.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE `agents` ADD `user_id` varchar(36) NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `projects` ADD `user_id` varchar(36) NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `agents` ADD CONSTRAINT `agents_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE `projects` ADD CONSTRAINT `projects_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_id` ON `agents` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_id` ON `projects` (`user_id`);
|
||||||
634
drizzle/migrations/meta/0002_snapshot.json
Normal file
634
drizzle/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "28e89c13-2bd0-4fcd-9b01-e8433c08d71e",
|
||||||
|
"prevId": "ada6a640-177d-4c35-b7e0-e1d20c9adabb",
|
||||||
|
"tables": {
|
||||||
|
"agents": {
|
||||||
|
"name": "agents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pod_name": {
|
||||||
|
"name": "pod_name",
|
||||||
|
"type": "varchar(253)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'agents'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('idle','busy','error','offline')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'idle'"
|
||||||
|
},
|
||||||
|
"current_task_id": {
|
||||||
|
"name": "current_task_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tasks_completed": {
|
||||||
|
"name": "tasks_completed",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"last_heartbeat": {
|
||||||
|
"name": "last_heartbeat",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"agents_user_id_users_id_fk": {
|
||||||
|
"name": "agents_user_id_users_id_fk",
|
||||||
|
"tableFrom": "agents",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"agents_id": {
|
||||||
|
"name": "agents_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"agents_pod_name_unique": {
|
||||||
|
"name": "agents_pod_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"pod_name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_id": {
|
||||||
|
"name": "gitea_repo_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_url": {
|
||||||
|
"name": "gitea_repo_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_owner": {
|
||||||
|
"name": "gitea_owner",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_name": {
|
||||||
|
"name": "gitea_repo_name",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"default_branch": {
|
||||||
|
"name": "default_branch",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'main'"
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"docker_image": {
|
||||||
|
"name": "docker_image",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_vars": {
|
||||||
|
"name": "env_vars",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"replicas": {
|
||||||
|
"name": "replicas",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"cpu_limit": {
|
||||||
|
"name": "cpu_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'500m'"
|
||||||
|
},
|
||||||
|
"memory_limit": {
|
||||||
|
"name": "memory_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'512Mi'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('active','paused','archived')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"projects_user_id_users_id_fk": {
|
||||||
|
"name": "projects_user_id_users_id_fk",
|
||||||
|
"tableFrom": "projects",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"projects_id": {
|
||||||
|
"name": "projects_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"projects_k8s_namespace_unique": {
|
||||||
|
"name": "projects_k8s_namespace_unique",
|
||||||
|
"columns": [
|
||||||
|
"k8s_namespace"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_expires_at": {
|
||||||
|
"name": "idx_expires_at",
|
||||||
|
"columns": [
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"sessions_id": {
|
||||||
|
"name": "sessions_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"name": "tasks",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"name": "priority",
|
||||||
|
"type": "enum('low','medium','high','urgent')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'medium'"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "enum('backlog','in_progress','needs_input','ready_to_test','approved','staging','production')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'backlog'"
|
||||||
|
},
|
||||||
|
"assigned_agent_id": {
|
||||||
|
"name": "assigned_agent_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"branch_name": {
|
||||||
|
"name": "branch_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pr_url": {
|
||||||
|
"name": "pr_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"preview_url": {
|
||||||
|
"name": "preview_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_project_state": {
|
||||||
|
"name": "idx_project_state",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tasks_project_id_projects_id_fk": {
|
||||||
|
"name": "tasks_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"tasks_assigned_agent_id_agents_id_fk": {
|
||||||
|
"name": "tasks_assigned_agent_id_agents_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "agents",
|
||||||
|
"columnsFrom": [
|
||||||
|
"assigned_agent_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"tasks_id": {
|
||||||
|
"name": "tasks_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_email": {
|
||||||
|
"name": "idx_email",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1768868010676,
|
"when": 1768868010676,
|
||||||
"tag": "0001_opposite_warbird",
|
"tag": "0001_opposite_warbird",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768924360408,
|
||||||
|
"tag": "0002_next_xorn",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
80
src/api/middleware/auth.ts
Normal file
80
src/api/middleware/auth.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Authentication Middleware
|
||||||
|
* Validates session cookies and extracts user ID
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from '../../db/client'
|
||||||
|
import { users, sessions } from '../../db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
export interface AuthContext {
|
||||||
|
userId: string
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate request using session cookie
|
||||||
|
* Returns null if authentication fails
|
||||||
|
*/
|
||||||
|
export async function authenticateRequest(req: Request): Promise<AuthContext | null> {
|
||||||
|
const sessionId = getSessionIdFromCookie(req)
|
||||||
|
if (!sessionId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query session with user join
|
||||||
|
const [result] = await db
|
||||||
|
.select({
|
||||||
|
sessionId: sessions.id,
|
||||||
|
sessionExpiresAt: sessions.expiresAt,
|
||||||
|
userId: users.id,
|
||||||
|
userEmail: users.email,
|
||||||
|
username: users.username,
|
||||||
|
})
|
||||||
|
.from(sessions)
|
||||||
|
.innerJoin(users, eq(sessions.userId, users.id))
|
||||||
|
.where(eq(sessions.id, sessionId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session expired
|
||||||
|
if (result.sessionExpiresAt < new Date()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: result.userId,
|
||||||
|
user: {
|
||||||
|
id: result.userId,
|
||||||
|
email: result.userEmail,
|
||||||
|
username: result.username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract session ID from cookie header
|
||||||
|
*/
|
||||||
|
function getSessionIdFromCookie(req: Request): string | null {
|
||||||
|
const cookieHeader = req.headers.get('cookie')
|
||||||
|
if (!cookieHeader) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
|
||||||
|
const [name, value] = cookie.trim().split('=')
|
||||||
|
if (name && value) {
|
||||||
|
acc[name] = value
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
return cookies.session || null
|
||||||
|
}
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
import { db } from '../../db/client'
|
import { db } from '../../db/client'
|
||||||
import { agents, tasks } from '../../db/schema'
|
import { agents, tasks } from '../../db/schema'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq, and } from 'drizzle-orm'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
import { authenticateRequest } from '../middleware/auth'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle all agent routes
|
* Handle all agent routes
|
||||||
@@ -15,7 +16,34 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
|
|||||||
const method = req.method
|
const method = req.method
|
||||||
const pathParts = url.pathname.split('/').filter(Boolean)
|
const pathParts = url.pathname.split('/').filter(Boolean)
|
||||||
|
|
||||||
// GET /api/agents - List all agents
|
// Authenticate user for protected endpoints
|
||||||
|
const publicEndpoints = ['heartbeat']
|
||||||
|
const isPublic = pathParts.length === 4 && publicEndpoints.includes(pathParts[3])
|
||||||
|
|
||||||
|
let auth = null
|
||||||
|
if (!isPublic) {
|
||||||
|
auth = await authenticateRequest(req)
|
||||||
|
if (!auth) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = auth?.userId
|
||||||
|
|
||||||
|
// GET /api/agents/my - List my agents (authenticated)
|
||||||
|
if (method === 'GET' && pathParts.length === 3 && pathParts[2] === 'my') {
|
||||||
|
return await listMyAgents(userId!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/agents/launch - Launch new agent (authenticated)
|
||||||
|
if (method === 'POST' && pathParts.length === 3 && pathParts[2] === 'launch') {
|
||||||
|
return await launchAgent(userId!, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/agents - List all agents (admin only - for now authenticated user sees all)
|
||||||
if (method === 'GET' && pathParts.length === 2) {
|
if (method === 'GET' && pathParts.length === 2) {
|
||||||
return await listAgents(url)
|
return await listAgents(url)
|
||||||
}
|
}
|
||||||
@@ -23,10 +51,10 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
|
|||||||
// GET /api/agents/:id - Get single agent
|
// GET /api/agents/:id - Get single agent
|
||||||
if (method === 'GET' && pathParts.length === 3) {
|
if (method === 'GET' && pathParts.length === 3) {
|
||||||
const agentId = pathParts[2]
|
const agentId = pathParts[2]
|
||||||
return await getAgent(agentId)
|
return await getAgent(agentId, userId!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/agents - Register agent
|
// POST /api/agents - Register agent (called by agent pod on startup)
|
||||||
if (method === 'POST' && pathParts.length === 2) {
|
if (method === 'POST' && pathParts.length === 2) {
|
||||||
return await registerAgent(req)
|
return await registerAgent(req)
|
||||||
}
|
}
|
||||||
@@ -34,10 +62,10 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
|
|||||||
// PATCH /api/agents/:id - Update agent
|
// PATCH /api/agents/:id - Update agent
|
||||||
if (method === 'PATCH' && pathParts.length === 3) {
|
if (method === 'PATCH' && pathParts.length === 3) {
|
||||||
const agentId = pathParts[2]
|
const agentId = pathParts[2]
|
||||||
return await updateAgent(agentId, req)
|
return await updateAgent(agentId, req, userId!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/agents/:id/heartbeat - Update heartbeat
|
// POST /api/agents/:id/heartbeat - Update heartbeat (public)
|
||||||
if (method === 'POST' && pathParts.length === 4 && pathParts[3] === 'heartbeat') {
|
if (method === 'POST' && pathParts.length === 4 && pathParts[3] === 'heartbeat') {
|
||||||
const agentId = pathParts[2]
|
const agentId = pathParts[2]
|
||||||
return await updateHeartbeat(agentId)
|
return await updateHeartbeat(agentId)
|
||||||
@@ -46,7 +74,7 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
|
|||||||
// DELETE /api/agents/:id - Unregister agent
|
// DELETE /api/agents/:id - Unregister agent
|
||||||
if (method === 'DELETE' && pathParts.length === 3) {
|
if (method === 'DELETE' && pathParts.length === 3) {
|
||||||
const agentId = pathParts[2]
|
const agentId = pathParts[2]
|
||||||
return await unregisterAgent(agentId)
|
return await unregisterAgent(agentId, userId!)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response('Not Found', { status: 404 })
|
return new Response('Not Found', { status: 404 })
|
||||||
@@ -82,14 +110,19 @@ async function listAgents(url: URL): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get single agent
|
* Get single agent (verify ownership)
|
||||||
*/
|
*/
|
||||||
async function getAgent(agentId: string): Promise<Response> {
|
async function getAgent(agentId: string, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const agent = await db
|
const agent = await db
|
||||||
.select()
|
.select()
|
||||||
.from(agents)
|
.from(agents)
|
||||||
.where(eq(agents.id, agentId))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(agents.id, agentId),
|
||||||
|
eq(agents.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (agent.length === 0) {
|
if (agent.length === 0) {
|
||||||
@@ -114,16 +147,17 @@ async function getAgent(agentId: string): Promise<Response> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register new agent (called when pod starts)
|
* Register new agent (called when pod starts)
|
||||||
|
* userId is extracted from env var passed to pod
|
||||||
*/
|
*/
|
||||||
async function registerAgent(req: Request): Promise<Response> {
|
async function registerAgent(req: Request): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!body.podName) {
|
if (!body.podName || !body.userId) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'podName is required',
|
error: 'podName and userId are required',
|
||||||
}, { status: 400 })
|
}, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +179,7 @@ async function registerAgent(req: Request): Promise<Response> {
|
|||||||
|
|
||||||
const newAgent = {
|
const newAgent = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
|
userId: body.userId,
|
||||||
podName: body.podName,
|
podName: body.podName,
|
||||||
k8sNamespace: body.k8sNamespace || 'agents',
|
k8sNamespace: body.k8sNamespace || 'agents',
|
||||||
status: 'idle' as const,
|
status: 'idle' as const,
|
||||||
@@ -169,23 +204,28 @@ async function registerAgent(req: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update agent status and current task
|
* Update agent status and current task (verify ownership)
|
||||||
*/
|
*/
|
||||||
async function updateAgent(agentId: string, req: Request): Promise<Response> {
|
async function updateAgent(agentId: string, req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
|
|
||||||
// Check if agent exists
|
// Check if agent exists and belongs to user
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
.from(agents)
|
.from(agents)
|
||||||
.where(eq(agents.id, agentId))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(agents.id, agentId),
|
||||||
|
eq(agents.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Agent not found',
|
error: 'Agent not found or access denied',
|
||||||
}, { status: 404 })
|
}, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,21 +300,26 @@ async function updateHeartbeat(agentId: string): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister agent (called when pod terminates)
|
* Unregister agent (called when pod terminates, verify ownership)
|
||||||
*/
|
*/
|
||||||
async function unregisterAgent(agentId: string): Promise<Response> {
|
async function unregisterAgent(agentId: string, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
// Check if agent exists
|
// Check if agent exists and belongs to user
|
||||||
const existing = await db
|
const existing = await db
|
||||||
.select()
|
.select()
|
||||||
.from(agents)
|
.from(agents)
|
||||||
.where(eq(agents.id, agentId))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(agents.id, agentId),
|
||||||
|
eq(agents.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Agent not found',
|
error: 'Agent not found or access denied',
|
||||||
}, { status: 404 })
|
}, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +331,11 @@ async function unregisterAgent(agentId: string): Promise<Response> {
|
|||||||
.where(eq(tasks.id, existing[0].currentTaskId))
|
.where(eq(tasks.id, existing[0].currentTaskId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete agent
|
// Delete agent from DB
|
||||||
await db.delete(agents).where(eq(agents.id, agentId))
|
await db.delete(agents).where(eq(agents.id, agentId))
|
||||||
|
|
||||||
|
// TODO: Delete K8s pod if it exists
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Agent unregistered',
|
message: 'Agent unregistered',
|
||||||
@@ -301,3 +348,68 @@ async function unregisterAgent(agentId: string): Promise<Response> {
|
|||||||
}, { status: 500 })
|
}, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List my agents (filtered by userId)
|
||||||
|
*/
|
||||||
|
async function listMyAgents(userId: string): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const myAgents = await db
|
||||||
|
.select()
|
||||||
|
.from(agents)
|
||||||
|
.where(eq(agents.userId, userId))
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: myAgents,
|
||||||
|
count: myAgents.length,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing my agents:', error)
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to list agents',
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch new agent (create pod dynamically)
|
||||||
|
* TODO: Integrate with K8s API to create pod
|
||||||
|
*/
|
||||||
|
async function launchAgent(userId: string, req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const agentId = randomUUID()
|
||||||
|
const podName = `claude-agent-${userId.slice(0, 8)}-${Date.now()}`
|
||||||
|
|
||||||
|
// Create agent record in DB
|
||||||
|
const newAgent = {
|
||||||
|
id: agentId,
|
||||||
|
userId,
|
||||||
|
podName,
|
||||||
|
k8sNamespace: 'agents',
|
||||||
|
status: 'idle' as const,
|
||||||
|
currentTaskId: null,
|
||||||
|
tasksCompleted: 0,
|
||||||
|
lastHeartbeat: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(agents).values(newAgent)
|
||||||
|
|
||||||
|
// TODO: Create K8s pod using K8s API
|
||||||
|
// For now, just return the agent record
|
||||||
|
// In production, this would call kubectl or use @kubernetes/client-node
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: newAgent,
|
||||||
|
message: 'Agent launch initiated. Pod will be created shortly.',
|
||||||
|
}, { status: 201 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error launching agent:', error)
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to launch agent',
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
import { db } from '../../db/client'
|
import { db } from '../../db/client'
|
||||||
import { tasks, agents, projects } from '../../db/schema'
|
import { tasks, agents, projects } from '../../db/schema'
|
||||||
import { eq, and, or } from 'drizzle-orm'
|
import { eq, and, or, inArray } from 'drizzle-orm'
|
||||||
|
import { authenticateRequest } from '../middleware/auth'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route handler for /api/mcp/*
|
* Route handler for /api/mcp/*
|
||||||
@@ -13,99 +14,117 @@ import { eq, and, or } from 'drizzle-orm'
|
|||||||
export async function handleMCPRoutes(req: Request, url: URL): Promise<Response> {
|
export async function handleMCPRoutes(req: Request, url: URL): Promise<Response> {
|
||||||
const path = url.pathname.replace('/api/mcp', '')
|
const path = url.pathname.replace('/api/mcp', '')
|
||||||
|
|
||||||
|
// /tools endpoint doesn't require authentication
|
||||||
|
if (path === '/tools' && req.method === 'GET') {
|
||||||
|
return handleGetTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate all other MCP requests
|
||||||
|
const auth = await authenticateRequest(req)
|
||||||
|
if (!auth) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = auth.userId
|
||||||
|
|
||||||
// POST /api/mcp/get_next_task
|
// POST /api/mcp/get_next_task
|
||||||
if (path === '/get_next_task' && req.method === 'POST') {
|
if (path === '/get_next_task' && req.method === 'POST') {
|
||||||
return await handleGetNextTask(req)
|
return await handleGetNextTask(req, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/mcp/update_task_status
|
// POST /api/mcp/update_task_status
|
||||||
if (path === '/update_task_status' && req.method === 'POST') {
|
if (path === '/update_task_status' && req.method === 'POST') {
|
||||||
return await handleUpdateTaskStatus(req)
|
return await handleUpdateTaskStatus(req, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/mcp/create_branch
|
// POST /api/mcp/create_branch
|
||||||
if (path === '/create_branch' && req.method === 'POST') {
|
if (path === '/create_branch' && req.method === 'POST') {
|
||||||
return await handleCreateBranch(req)
|
return await handleCreateBranch(req, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/mcp/create_pull_request
|
// POST /api/mcp/create_pull_request
|
||||||
if (path === '/create_pull_request' && req.method === 'POST') {
|
if (path === '/create_pull_request' && req.method === 'POST') {
|
||||||
return await handleCreatePullRequest(req)
|
return await handleCreatePullRequest(req, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/mcp/ask_user_question
|
// POST /api/mcp/ask_user_question
|
||||||
if (path === '/ask_user_question' && req.method === 'POST') {
|
if (path === '/ask_user_question' && req.method === 'POST') {
|
||||||
return await handleAskUserQuestion(req)
|
return await handleAskUserQuestion(req, userId)
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/mcp/tools - List available tools
|
|
||||||
if (path === '/tools' && req.method === 'GET') {
|
|
||||||
return Response.json({
|
|
||||||
success: true,
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
name: 'get_next_task',
|
|
||||||
description: 'Get the next available task from the backlog',
|
|
||||||
endpoint: '/api/mcp/get_next_task',
|
|
||||||
method: 'POST',
|
|
||||||
params: {
|
|
||||||
agentId: 'string (required)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'update_task_status',
|
|
||||||
description: 'Update the status of a task',
|
|
||||||
endpoint: '/api/mcp/update_task_status',
|
|
||||||
method: 'POST',
|
|
||||||
params: {
|
|
||||||
taskId: 'string (required)',
|
|
||||||
status: 'string (required)',
|
|
||||||
errorMessage: 'string (optional)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_branch',
|
|
||||||
description: 'Create a Git branch for a task',
|
|
||||||
endpoint: '/api/mcp/create_branch',
|
|
||||||
method: 'POST',
|
|
||||||
params: {
|
|
||||||
taskId: 'string (required)',
|
|
||||||
branchName: 'string (required)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_pull_request',
|
|
||||||
description: 'Create a pull request in Gitea',
|
|
||||||
endpoint: '/api/mcp/create_pull_request',
|
|
||||||
method: 'POST',
|
|
||||||
params: {
|
|
||||||
taskId: 'string (required)',
|
|
||||||
title: 'string (required)',
|
|
||||||
description: 'string (optional)',
|
|
||||||
branchName: 'string (required)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ask_user_question',
|
|
||||||
description: 'Ask the user a question',
|
|
||||||
endpoint: '/api/mcp/ask_user_question',
|
|
||||||
method: 'POST',
|
|
||||||
params: {
|
|
||||||
taskId: 'string (required)',
|
|
||||||
question: 'string (required)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ message: 'Not found' }, { status: 404 })
|
return Response.json({ message: 'Not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get next available task
|
* Get available tools (public endpoint)
|
||||||
*/
|
*/
|
||||||
async function handleGetNextTask(req: Request): Promise<Response> {
|
function handleGetTools(): Response {
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'get_next_task',
|
||||||
|
description: 'Get the next available task from the backlog',
|
||||||
|
endpoint: '/api/mcp/get_next_task',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
agentId: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_task_status',
|
||||||
|
description: 'Update the status of a task',
|
||||||
|
endpoint: '/api/mcp/update_task_status',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
status: 'string (required)',
|
||||||
|
errorMessage: 'string (optional)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_branch',
|
||||||
|
description: 'Create a Git branch for a task',
|
||||||
|
endpoint: '/api/mcp/create_branch',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
branchName: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_pull_request',
|
||||||
|
description: 'Create a pull request in Gitea',
|
||||||
|
endpoint: '/api/mcp/create_pull_request',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
title: 'string (required)',
|
||||||
|
description: 'string (optional)',
|
||||||
|
branchName: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ask_user_question',
|
||||||
|
description: 'Ask the user a question',
|
||||||
|
endpoint: '/api/mcp/ask_user_question',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
question: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next available task (filtered by user's projects)
|
||||||
|
*/
|
||||||
|
async function handleGetNextTask(req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { agentId } = body
|
const { agentId } = body
|
||||||
@@ -117,11 +136,46 @@ async function handleGetNextTask(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get next task in backlog, ordered by priority
|
// Verify agent belongs to this user
|
||||||
|
const [agent] = await db
|
||||||
|
.select()
|
||||||
|
.from(agents)
|
||||||
|
.where(and(eq(agents.id, agentId), eq(agents.userId, userId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Agent not found or does not belong to user' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's projects
|
||||||
|
const userProjects = await db
|
||||||
|
.select({ id: projects.id })
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.userId, userId))
|
||||||
|
|
||||||
|
if (userProjects.length === 0) {
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: 'No projects available for this user',
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectIds = userProjects.map(p => p.id)
|
||||||
|
|
||||||
|
// Get next task in backlog from user's projects, ordered by priority
|
||||||
const [task] = await db
|
const [task] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(tasks)
|
.from(tasks)
|
||||||
.where(eq(tasks.state, 'backlog'))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tasks.state, 'backlog'),
|
||||||
|
inArray(tasks.projectId, projectIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
.orderBy(tasks.priority, tasks.createdAt)
|
.orderBy(tasks.priority, tasks.createdAt)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
@@ -175,9 +229,9 @@ async function handleGetNextTask(req: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update task status
|
* Update task status (verify user owns the task's project)
|
||||||
*/
|
*/
|
||||||
async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
async function handleUpdateTaskStatus(req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { taskId, status, errorMessage } = body
|
const { taskId, status, errorMessage } = body
|
||||||
@@ -206,6 +260,26 @@ async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify task belongs to user's project
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.innerJoin(projects, eq(tasks.projectId, projects.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tasks.id, taskId),
|
||||||
|
eq(projects.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Task not found or access denied' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(tasks)
|
.update(tasks)
|
||||||
.set({
|
.set({
|
||||||
@@ -216,13 +290,13 @@ async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
|||||||
|
|
||||||
// If task is completed, update agent
|
// If task is completed, update agent
|
||||||
if (['ready_to_test', 'approved'].includes(status)) {
|
if (['ready_to_test', 'approved'].includes(status)) {
|
||||||
const [task] = await db
|
const [taskData] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(tasks)
|
.from(tasks)
|
||||||
.where(eq(tasks.id, taskId))
|
.where(eq(tasks.id, taskId))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (task?.assignedAgentId) {
|
if (taskData?.assignedAgentId) {
|
||||||
await db
|
await db
|
||||||
.update(agents)
|
.update(agents)
|
||||||
.set({
|
.set({
|
||||||
@@ -230,7 +304,7 @@ async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
|||||||
currentTaskId: null,
|
currentTaskId: null,
|
||||||
tasksCompleted: db.sql`${agents.tasksCompleted} + 1`,
|
tasksCompleted: db.sql`${agents.tasksCompleted} + 1`,
|
||||||
})
|
})
|
||||||
.where(eq(agents.id, task.assignedAgentId))
|
.where(eq(agents.id, taskData.assignedAgentId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,9 +322,9 @@ async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create branch
|
* Create branch (verify user owns the task's project)
|
||||||
*/
|
*/
|
||||||
async function handleCreateBranch(req: Request): Promise<Response> {
|
async function handleCreateBranch(req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { taskId, branchName } = body
|
const { taskId, branchName } = body
|
||||||
@@ -262,6 +336,26 @@ async function handleCreateBranch(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify task belongs to user's project
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.innerJoin(projects, eq(tasks.projectId, projects.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tasks.id, taskId),
|
||||||
|
eq(projects.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Task not found or access denied' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(tasks)
|
.update(tasks)
|
||||||
.set({
|
.set({
|
||||||
@@ -283,9 +377,9 @@ async function handleCreateBranch(req: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create pull request
|
* Create pull request (verify user owns the task's project)
|
||||||
*/
|
*/
|
||||||
async function handleCreatePullRequest(req: Request): Promise<Response> {
|
async function handleCreatePullRequest(req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { taskId, title, description, branchName } = body
|
const { taskId, title, description, branchName } = body
|
||||||
@@ -297,6 +391,26 @@ async function handleCreatePullRequest(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify task belongs to user's project
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.innerJoin(projects, eq(tasks.projectId, projects.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tasks.id, taskId),
|
||||||
|
eq(projects.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Task not found or access denied' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Integrate with Gitea API to actually create PR
|
// TODO: Integrate with Gitea API to actually create PR
|
||||||
// For now, just update task with placeholder PR URL
|
// For now, just update task with placeholder PR URL
|
||||||
const prUrl = `https://git.fuq.tv/pulls/${taskId}`
|
const prUrl = `https://git.fuq.tv/pulls/${taskId}`
|
||||||
@@ -326,9 +440,9 @@ async function handleCreatePullRequest(req: Request): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask user question
|
* Ask user question (verify user owns the task's project)
|
||||||
*/
|
*/
|
||||||
async function handleAskUserQuestion(req: Request): Promise<Response> {
|
async function handleAskUserQuestion(req: Request, userId: string): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { taskId, question } = body
|
const { taskId, question } = body
|
||||||
@@ -340,6 +454,26 @@ async function handleAskUserQuestion(req: Request): Promise<Response> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify task belongs to user's project
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.innerJoin(projects, eq(tasks.projectId, projects.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(tasks.id, taskId),
|
||||||
|
eq(projects.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Task not found or access denied' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(tasks)
|
.update(tasks)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const sessions = mysqlTable('sessions', {
|
|||||||
|
|
||||||
export const projects = mysqlTable('projects', {
|
export const projects = mysqlTable('projects', {
|
||||||
id: varchar('id', { length: 36 }).primaryKey(),
|
id: varchar('id', { length: 36 }).primaryKey(),
|
||||||
|
userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
name: varchar('name', { length: 255 }).notNull(),
|
name: varchar('name', { length: 255 }).notNull(),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ export const projects = mysqlTable('projects', {
|
|||||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
statusIdx: index('idx_status').on(table.status),
|
statusIdx: index('idx_status').on(table.status),
|
||||||
|
userIdIdx: index('idx_user_id').on(table.userId),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -92,6 +94,7 @@ export const projects = mysqlTable('projects', {
|
|||||||
|
|
||||||
export const agents = mysqlTable('agents', {
|
export const agents = mysqlTable('agents', {
|
||||||
id: varchar('id', { length: 36 }).primaryKey(),
|
id: varchar('id', { length: 36 }).primaryKey(),
|
||||||
|
userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
|
||||||
// K8s
|
// K8s
|
||||||
podName: varchar('pod_name', { length: 253 }).notNull().unique(),
|
podName: varchar('pod_name', { length: 253 }).notNull().unique(),
|
||||||
@@ -110,6 +113,7 @@ export const agents = mysqlTable('agents', {
|
|||||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
statusIdx: index('idx_status').on(table.status),
|
statusIdx: index('idx_status').on(table.status),
|
||||||
|
userIdIdx: index('idx_user_id').on(table.userId),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -157,7 +161,24 @@ export const tasks = mysqlTable('tasks', {
|
|||||||
// RELATIONS
|
// RELATIONS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export const projectsRelations = relations(projects, ({ many }) => ({
|
export const usersRelations = relations(users, ({ many }) => ({
|
||||||
|
sessions: many(sessions),
|
||||||
|
projects: many(projects),
|
||||||
|
agents: many(agents),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [sessions.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [projects.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
tasks: many(tasks),
|
tasks: many(tasks),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -173,6 +194,10 @@ export const tasksRelations = relations(tasks, ({ one }) => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
export const agentsRelations = relations(agents, ({ one }) => ({
|
export const agentsRelations = relations(agents, ({ one }) => ({
|
||||||
|
user: one(users, {
|
||||||
|
fields: [agents.userId],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
currentTask: one(tasks, {
|
currentTask: one(tasks, {
|
||||||
fields: [agents.currentTaskId],
|
fields: [agents.currentTaskId],
|
||||||
references: [tasks.id],
|
references: [tasks.id],
|
||||||
|
|||||||
58
src/index.ts
58
src/index.ts
@@ -4,8 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { runMigrations } from './db/migrate'
|
import { runMigrations } from './db/migrate'
|
||||||
import { testConnection } from './db/client'
|
import { testConnection, db } from './db/client'
|
||||||
import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes, handleMCPRoutes } from './api/routes'
|
import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes, handleMCPRoutes } from './api/routes'
|
||||||
|
import { authenticateRequest } from './api/middleware/auth'
|
||||||
|
import { agents } from './db/schema'
|
||||||
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
|
||||||
console.log('🚀 Starting AiWorker Backend...')
|
console.log('🚀 Starting AiWorker Backend...')
|
||||||
console.log(`Bun version: ${Bun.version}`)
|
console.log(`Bun version: ${Bun.version}`)
|
||||||
@@ -64,6 +67,59 @@ const server = Bun.serve({
|
|||||||
return handleMCPRoutes(req, url)
|
return handleMCPRoutes(req, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent terminal proxy
|
||||||
|
if (url.pathname.startsWith('/agent-terminal/')) {
|
||||||
|
const pathParts = url.pathname.split('/').filter(Boolean)
|
||||||
|
const agentId = pathParts[1]
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
const auth = await authenticateRequest(req)
|
||||||
|
if (!auth) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get agent and verify ownership
|
||||||
|
const [agent] = await db
|
||||||
|
.select()
|
||||||
|
.from(agents)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(agents.id, agentId),
|
||||||
|
eq(agents.userId, auth.userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Agent not found or access denied' },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy to agent terminal
|
||||||
|
const agentUrl = `http://${agent.podName}.agents.svc.cluster.local:7681${url.pathname.replace(`/agent-terminal/${agentId}`, '')}${url.search}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(agentUrl, {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Terminal proxy error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Failed to connect to agent terminal' },
|
||||||
|
{ status: 502 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generic API info
|
// Generic API info
|
||||||
if (url.pathname.startsWith('/api/')) {
|
if (url.pathname.startsWith('/api/')) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
|
|||||||
Reference in New Issue
Block a user