feat(agents): Add identity and soul template fields to board creation

This commit is contained in:
Abhimanyu Saharan
2026-02-04 20:21:33 +05:30
parent 1c972edb46
commit c3357f92d9
117 changed files with 7899 additions and 1339 deletions

View File

@@ -1,27 +0,0 @@
"""add agent heartbeat config column
Revision ID: 2b4c2f7b3eda
Revises: 69858cb75533
Create Date: 2026-02-04 16:36:55.587762
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = '2b4c2f7b3eda'
down_revision = '69858cb75533'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS heartbeat_config JSON"
)
def downgrade() -> None:
op.execute("ALTER TABLE agents DROP COLUMN IF EXISTS heartbeat_config")

View File

@@ -1,574 +0,0 @@
"""init
Revision ID: 5630abfa60f8
Revises:
Create Date: 2026-02-03 17:52:47.887105
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "5630abfa60f8"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"orgs",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_orgs_slug"), "orgs", ["slug"], unique=True)
op.create_table(
"users",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("clerk_user_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("is_super_admin", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_users_clerk_user_id"), "users", ["clerk_user_id"], unique=True)
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=False)
op.create_table(
"workspaces",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_workspaces_org_id"), "workspaces", ["org_id"], unique=False)
op.create_index(op.f("ix_workspaces_slug"), "workspaces", ["slug"], unique=False)
op.create_table(
"agents",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("role", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("openclaw_session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("api_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("api_token_last_used_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_agents_openclaw_session_id"), "agents", ["openclaw_session_id"], unique=False
)
op.create_index(op.f("ix_agents_org_id"), "agents", ["org_id"], unique=False)
op.create_index(op.f("ix_agents_workspace_id"), "agents", ["workspace_id"], unique=False)
op.create_table(
"gateway_configs",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=True),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("base_url", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_gateway_configs_org_id"), "gateway_configs", ["org_id"], unique=False)
op.create_index(
op.f("ix_gateway_configs_workspace_id"), "gateway_configs", ["workspace_id"], unique=False
)
op.create_table(
"memberships",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("role", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_memberships_org_id"), "memberships", ["org_id"], unique=False)
op.create_index(op.f("ix_memberships_user_id"), "memberships", ["user_id"], unique=False)
op.create_index(
op.f("ix_memberships_workspace_id"), "memberships", ["workspace_id"], unique=False
)
op.create_table(
"orchestration_templates",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("kind", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("template_markdown", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_orchestration_templates_kind"), "orchestration_templates", ["kind"], unique=False
)
op.create_index(
op.f("ix_orchestration_templates_org_id"),
"orchestration_templates",
["org_id"],
unique=False,
)
op.create_index(
op.f("ix_orchestration_templates_workspace_id"),
"orchestration_templates",
["workspace_id"],
unique=False,
)
op.create_table(
"projects",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_projects_org_id"), "projects", ["org_id"], unique=False)
op.create_index(op.f("ix_projects_status"), "projects", ["status"], unique=False)
op.create_index(op.f("ix_projects_workspace_id"), "projects", ["workspace_id"], unique=False)
op.create_table(
"workspace_api_tokens",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("label", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("last_used_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_workspace_api_tokens_org_id"), "workspace_api_tokens", ["org_id"], unique=False
)
op.create_index(
op.f("ix_workspace_api_tokens_token_hash"),
"workspace_api_tokens",
["token_hash"],
unique=True,
)
op.create_index(
op.f("ix_workspace_api_tokens_workspace_id"),
"workspace_api_tokens",
["workspace_id"],
unique=False,
)
op.create_table(
"openclaw_sessions",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("agent_id", sa.Uuid(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("last_seen_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["agent_id"],
["agents.id"],
),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_openclaw_sessions_org_id"), "openclaw_sessions", ["org_id"], unique=False
)
op.create_index(
op.f("ix_openclaw_sessions_session_id"), "openclaw_sessions", ["session_id"], unique=True
)
op.create_index(
op.f("ix_openclaw_sessions_status"), "openclaw_sessions", ["status"], unique=False
)
op.create_index(
op.f("ix_openclaw_sessions_workspace_id"),
"openclaw_sessions",
["workspace_id"],
unique=False,
)
op.create_table(
"tasks",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("priority", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("due_at", sa.DateTime(), nullable=True),
sa.Column("assigned_agent_id", sa.Uuid(), nullable=True),
sa.Column("created_by_user_id", sa.Uuid(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["assigned_agent_id"],
["agents.id"],
),
sa.ForeignKeyConstraint(
["created_by_user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_tasks_assigned_agent_id"), "tasks", ["assigned_agent_id"], unique=False
)
op.create_index(
op.f("ix_tasks_created_by_user_id"), "tasks", ["created_by_user_id"], unique=False
)
op.create_index(op.f("ix_tasks_org_id"), "tasks", ["org_id"], unique=False)
op.create_index(op.f("ix_tasks_priority"), "tasks", ["priority"], unique=False)
op.create_index(op.f("ix_tasks_project_id"), "tasks", ["project_id"], unique=False)
op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False)
op.create_index(op.f("ix_tasks_workspace_id"), "tasks", ["workspace_id"], unique=False)
op.create_table(
"task_activities",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("task_id", sa.Uuid(), nullable=False),
sa.Column("activity_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("actor_user_id", sa.Uuid(), nullable=True),
sa.Column("actor_agent_id", sa.Uuid(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["actor_agent_id"],
["agents.id"],
),
sa.ForeignKeyConstraint(
["actor_user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["task_id"],
["tasks.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_task_activities_org_id"), "task_activities", ["org_id"], unique=False)
op.create_index(
op.f("ix_task_activities_task_id"), "task_activities", ["task_id"], unique=False
)
op.create_index(
op.f("ix_task_activities_workspace_id"), "task_activities", ["workspace_id"], unique=False
)
op.create_table(
"task_deliverables",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("task_id", sa.Uuid(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("markdown_content", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_by_user_id", sa.Uuid(), nullable=True),
sa.Column("created_by_agent_id", sa.Uuid(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["created_by_agent_id"],
["agents.id"],
),
sa.ForeignKeyConstraint(
["created_by_user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["task_id"],
["tasks.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_task_deliverables_org_id"), "task_deliverables", ["org_id"], unique=False
)
op.create_index(
op.f("ix_task_deliverables_task_id"), "task_deliverables", ["task_id"], unique=False
)
op.create_index(
op.f("ix_task_deliverables_workspace_id"),
"task_deliverables",
["workspace_id"],
unique=False,
)
op.create_table(
"task_status_history",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("task_id", sa.Uuid(), nullable=False),
sa.Column("from_status", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("to_status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("actor_user_id", sa.Uuid(), nullable=True),
sa.Column("actor_agent_id", sa.Uuid(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["actor_agent_id"],
["agents.id"],
),
sa.ForeignKeyConstraint(
["actor_user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["task_id"],
["tasks.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_task_status_history_org_id"), "task_status_history", ["org_id"], unique=False
)
op.create_index(
op.f("ix_task_status_history_task_id"), "task_status_history", ["task_id"], unique=False
)
op.create_index(
op.f("ix_task_status_history_workspace_id"),
"task_status_history",
["workspace_id"],
unique=False,
)
op.create_table(
"task_subagents",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("task_id", sa.Uuid(), nullable=False),
sa.Column("agent_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("openclaw_session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["task_id"],
["tasks.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_task_subagents_org_id"), "task_subagents", ["org_id"], unique=False)
op.create_index(op.f("ix_task_subagents_task_id"), "task_subagents", ["task_id"], unique=False)
op.create_index(
op.f("ix_task_subagents_workspace_id"), "task_subagents", ["workspace_id"], unique=False
)
op.create_table(
"transcripts",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("task_id", sa.Uuid(), nullable=True),
sa.Column("session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("full_text", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["task_id"],
["tasks.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_transcripts_org_id"), "transcripts", ["org_id"], unique=False)
op.create_index(op.f("ix_transcripts_session_id"), "transcripts", ["session_id"], unique=False)
op.create_index(op.f("ix_transcripts_task_id"), "transcripts", ["task_id"], unique=False)
op.create_index(
op.f("ix_transcripts_workspace_id"), "transcripts", ["workspace_id"], unique=False
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_transcripts_workspace_id"), table_name="transcripts")
op.drop_index(op.f("ix_transcripts_task_id"), table_name="transcripts")
op.drop_index(op.f("ix_transcripts_session_id"), table_name="transcripts")
op.drop_index(op.f("ix_transcripts_org_id"), table_name="transcripts")
op.drop_table("transcripts")
op.drop_index(op.f("ix_task_subagents_workspace_id"), table_name="task_subagents")
op.drop_index(op.f("ix_task_subagents_task_id"), table_name="task_subagents")
op.drop_index(op.f("ix_task_subagents_org_id"), table_name="task_subagents")
op.drop_table("task_subagents")
op.drop_index(op.f("ix_task_status_history_workspace_id"), table_name="task_status_history")
op.drop_index(op.f("ix_task_status_history_task_id"), table_name="task_status_history")
op.drop_index(op.f("ix_task_status_history_org_id"), table_name="task_status_history")
op.drop_table("task_status_history")
op.drop_index(op.f("ix_task_deliverables_workspace_id"), table_name="task_deliverables")
op.drop_index(op.f("ix_task_deliverables_task_id"), table_name="task_deliverables")
op.drop_index(op.f("ix_task_deliverables_org_id"), table_name="task_deliverables")
op.drop_table("task_deliverables")
op.drop_index(op.f("ix_task_activities_workspace_id"), table_name="task_activities")
op.drop_index(op.f("ix_task_activities_task_id"), table_name="task_activities")
op.drop_index(op.f("ix_task_activities_org_id"), table_name="task_activities")
op.drop_table("task_activities")
op.drop_index(op.f("ix_tasks_workspace_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_status"), table_name="tasks")
op.drop_index(op.f("ix_tasks_project_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_priority"), table_name="tasks")
op.drop_index(op.f("ix_tasks_org_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_created_by_user_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_assigned_agent_id"), table_name="tasks")
op.drop_table("tasks")
op.drop_index(op.f("ix_openclaw_sessions_workspace_id"), table_name="openclaw_sessions")
op.drop_index(op.f("ix_openclaw_sessions_status"), table_name="openclaw_sessions")
op.drop_index(op.f("ix_openclaw_sessions_session_id"), table_name="openclaw_sessions")
op.drop_index(op.f("ix_openclaw_sessions_org_id"), table_name="openclaw_sessions")
op.drop_table("openclaw_sessions")
op.drop_index(op.f("ix_workspace_api_tokens_workspace_id"), table_name="workspace_api_tokens")
op.drop_index(op.f("ix_workspace_api_tokens_token_hash"), table_name="workspace_api_tokens")
op.drop_index(op.f("ix_workspace_api_tokens_org_id"), table_name="workspace_api_tokens")
op.drop_table("workspace_api_tokens")
op.drop_index(op.f("ix_projects_workspace_id"), table_name="projects")
op.drop_index(op.f("ix_projects_status"), table_name="projects")
op.drop_index(op.f("ix_projects_org_id"), table_name="projects")
op.drop_table("projects")
op.drop_index(
op.f("ix_orchestration_templates_workspace_id"), table_name="orchestration_templates"
)
op.drop_index(op.f("ix_orchestration_templates_org_id"), table_name="orchestration_templates")
op.drop_index(op.f("ix_orchestration_templates_kind"), table_name="orchestration_templates")
op.drop_table("orchestration_templates")
op.drop_index(op.f("ix_memberships_workspace_id"), table_name="memberships")
op.drop_index(op.f("ix_memberships_user_id"), table_name="memberships")
op.drop_index(op.f("ix_memberships_org_id"), table_name="memberships")
op.drop_table("memberships")
op.drop_index(op.f("ix_gateway_configs_workspace_id"), table_name="gateway_configs")
op.drop_index(op.f("ix_gateway_configs_org_id"), table_name="gateway_configs")
op.drop_table("gateway_configs")
op.drop_index(op.f("ix_agents_workspace_id"), table_name="agents")
op.drop_index(op.f("ix_agents_org_id"), table_name="agents")
op.drop_index(op.f("ix_agents_openclaw_session_id"), table_name="agents")
op.drop_table("agents")
op.drop_index(op.f("ix_workspaces_slug"), table_name="workspaces")
op.drop_index(op.f("ix_workspaces_org_id"), table_name="workspaces")
op.drop_table("workspaces")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_index(op.f("ix_users_clerk_user_id"), table_name="users")
op.drop_table("users")
op.drop_index(op.f("ix_orgs_slug"), table_name="orgs")
op.drop_table("orgs")
# ### end Alembic commands ###

View File

@@ -1,29 +0,0 @@
"""add agent heartbeat config
Revision ID: 69858cb75533
Revises: f1a2b3c4d5e6
Create Date: 2026-02-04 16:32:42.028772
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = '69858cb75533'
down_revision = 'f1a2b3c4d5e6'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS heartbeat_config JSON"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS heartbeat_config"
)

View File

@@ -1,41 +0,0 @@
"""add agent provision confirmation
Revision ID: 6df47d330227
Revises: e0f28e965fa5
Create Date: 2026-02-04 17:16:44.472239
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = '6df47d330227'
down_revision = 'e0f28e965fa5'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS provision_requested_at TIMESTAMP"
)
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS provision_confirm_token_hash VARCHAR"
)
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS provision_action VARCHAR"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS provision_action"
)
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS provision_confirm_token_hash"
)
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS provision_requested_at"
)

View File

@@ -1,64 +0,0 @@
"""add boards and task board id
Revision ID: 7e3d9b8c1f4a
Revises: 5630abfa60f8
Create Date: 2026-02-03 20:12:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "7e3d9b8c1f4a"
down_revision = "5630abfa60f8"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"boards",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("slug", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_boards_org_id"), "boards", ["org_id"], unique=False)
op.create_index(
op.f("ix_boards_workspace_id"), "boards", ["workspace_id"], unique=False
)
op.create_index(op.f("ix_boards_slug"), "boards", ["slug"], unique=False)
op.add_column("tasks", sa.Column("board_id", sa.Uuid(), nullable=True))
op.create_index(op.f("ix_tasks_board_id"), "tasks", ["board_id"], unique=False)
op.create_foreign_key(
"fk_tasks_board_id_boards", "tasks", "boards", ["board_id"], ["id"]
)
def downgrade() -> None:
op.drop_constraint("fk_tasks_board_id_boards", "tasks", type_="foreignkey")
op.drop_index(op.f("ix_tasks_board_id"), table_name="tasks")
op.drop_column("tasks", "board_id")
op.drop_index(op.f("ix_boards_slug"), table_name="boards")
op.drop_index(op.f("ix_boards_workspace_id"), table_name="boards")
op.drop_index(op.f("ix_boards_org_id"), table_name="boards")
op.drop_table("boards")

View File

@@ -1,36 +0,0 @@
"""add task assigned agent
Revision ID: 8045fbfb157f
Revises: 6df47d330227
Create Date: 2026-02-04 17:28:57.465934
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = '8045fbfb157f'
down_revision = '6df47d330227'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assigned_agent_id UUID"
)
op.execute(
"ALTER TABLE tasks ADD CONSTRAINT IF NOT EXISTS tasks_assigned_agent_id_fkey "
"FOREIGN KEY (assigned_agent_id) REFERENCES agents(id)"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_assigned_agent_id_fkey"
)
op.execute(
"ALTER TABLE tasks DROP COLUMN IF EXISTS assigned_agent_id"
)

View File

@@ -1,63 +0,0 @@
"""drop projects and task project_id
Revision ID: 8b6d1b8f4b21
Revises: 7e3d9b8c1f4a
Create Date: 2026-02-03 23:05:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "8b6d1b8f4b21"
down_revision = "7e3d9b8c1f4a"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.drop_constraint("tasks_project_id_fkey", "tasks", type_="foreignkey")
op.drop_index(op.f("ix_tasks_project_id"), table_name="tasks")
op.drop_column("tasks", "project_id")
op.drop_index(op.f("ix_projects_workspace_id"), table_name="projects")
op.drop_index(op.f("ix_projects_status"), table_name="projects")
op.drop_index(op.f("ix_projects_org_id"), table_name="projects")
op.drop_table("projects")
def downgrade() -> None:
op.create_table(
"projects",
sa.Column("org_id", sa.Uuid(), nullable=False),
sa.Column("workspace_id", sa.Uuid(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["org_id"],
["orgs.id"],
),
sa.ForeignKeyConstraint(
["workspace_id"],
["workspaces.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_projects_org_id"), "projects", ["org_id"], unique=False)
op.create_index(op.f("ix_projects_status"), "projects", ["status"], unique=False)
op.create_index(op.f("ix_projects_workspace_id"), "projects", ["workspace_id"], unique=False)
op.add_column("tasks", sa.Column("project_id", sa.Uuid(), nullable=True))
op.create_index(op.f("ix_tasks_project_id"), "tasks", ["project_id"], unique=False)
op.create_foreign_key(
"tasks_project_id_fkey", "tasks", "projects", ["project_id"], ["id"]
)

View File

@@ -0,0 +1,147 @@
"""init
Revision ID: 939a1d2dc607
Revises:
Create Date: 2026-02-04 19:34:33.600751
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = '939a1d2dc607'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('boards',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('slug', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('gateway_url', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('gateway_token', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('gateway_main_session_key', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('gateway_workspace_root', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('identity_template', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('soul_template', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_boards_slug'), 'boards', ['slug'], unique=False)
op.create_table('users',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('clerk_user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('preferred_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('pronouns', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('timezone', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('context', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('is_super_admin', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_clerk_user_id'), 'users', ['clerk_user_id'], unique=True)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=False)
op.create_table('agents',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('board_id', sa.Uuid(), nullable=True),
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('openclaw_session_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('agent_token_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('heartbeat_config', sa.JSON(), nullable=True),
sa.Column('provision_requested_at', sa.DateTime(), nullable=True),
sa.Column('provision_confirm_token_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('provision_action', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('delete_requested_at', sa.DateTime(), nullable=True),
sa.Column('delete_confirm_token_hash', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('last_seen_at', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['board_id'], ['boards.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_agents_agent_token_hash'), 'agents', ['agent_token_hash'], unique=False)
op.create_index(op.f('ix_agents_board_id'), 'agents', ['board_id'], unique=False)
op.create_index(op.f('ix_agents_delete_confirm_token_hash'), 'agents', ['delete_confirm_token_hash'], unique=False)
op.create_index(op.f('ix_agents_name'), 'agents', ['name'], unique=False)
op.create_index(op.f('ix_agents_openclaw_session_id'), 'agents', ['openclaw_session_id'], unique=False)
op.create_index(op.f('ix_agents_provision_action'), 'agents', ['provision_action'], unique=False)
op.create_index(op.f('ix_agents_provision_confirm_token_hash'), 'agents', ['provision_confirm_token_hash'], unique=False)
op.create_index(op.f('ix_agents_status'), 'agents', ['status'], unique=False)
op.create_table('tasks',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('board_id', sa.Uuid(), nullable=True),
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('priority', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('due_at', sa.DateTime(), nullable=True),
sa.Column('in_progress_at', sa.DateTime(), nullable=True),
sa.Column('created_by_user_id', sa.Uuid(), nullable=True),
sa.Column('assigned_agent_id', sa.Uuid(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['assigned_agent_id'], ['agents.id'], ),
sa.ForeignKeyConstraint(['board_id'], ['boards.id'], ),
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tasks_assigned_agent_id'), 'tasks', ['assigned_agent_id'], unique=False)
op.create_index(op.f('ix_tasks_board_id'), 'tasks', ['board_id'], unique=False)
op.create_index(op.f('ix_tasks_created_by_user_id'), 'tasks', ['created_by_user_id'], unique=False)
op.create_index(op.f('ix_tasks_priority'), 'tasks', ['priority'], unique=False)
op.create_index(op.f('ix_tasks_status'), 'tasks', ['status'], unique=False)
op.create_table('activity_events',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('event_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('message', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column('agent_id', sa.Uuid(), nullable=True),
sa.Column('task_id', sa.Uuid(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ),
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_activity_events_agent_id'), 'activity_events', ['agent_id'], unique=False)
op.create_index(op.f('ix_activity_events_event_type'), 'activity_events', ['event_type'], unique=False)
op.create_index(op.f('ix_activity_events_task_id'), 'activity_events', ['task_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_activity_events_task_id'), table_name='activity_events')
op.drop_index(op.f('ix_activity_events_event_type'), table_name='activity_events')
op.drop_index(op.f('ix_activity_events_agent_id'), table_name='activity_events')
op.drop_table('activity_events')
op.drop_index(op.f('ix_tasks_status'), table_name='tasks')
op.drop_index(op.f('ix_tasks_priority'), table_name='tasks')
op.drop_index(op.f('ix_tasks_created_by_user_id'), table_name='tasks')
op.drop_index(op.f('ix_tasks_board_id'), table_name='tasks')
op.drop_index(op.f('ix_tasks_assigned_agent_id'), table_name='tasks')
op.drop_table('tasks')
op.drop_index(op.f('ix_agents_status'), table_name='agents')
op.drop_index(op.f('ix_agents_provision_confirm_token_hash'), table_name='agents')
op.drop_index(op.f('ix_agents_provision_action'), table_name='agents')
op.drop_index(op.f('ix_agents_openclaw_session_id'), table_name='agents')
op.drop_index(op.f('ix_agents_name'), table_name='agents')
op.drop_index(op.f('ix_agents_delete_confirm_token_hash'), table_name='agents')
op.drop_index(op.f('ix_agents_board_id'), table_name='agents')
op.drop_index(op.f('ix_agents_agent_token_hash'), table_name='agents')
op.drop_table('agents')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_index(op.f('ix_users_clerk_user_id'), table_name='users')
op.drop_table('users')
op.drop_index(op.f('ix_boards_slug'), table_name='boards')
op.drop_table('boards')
# ### end Alembic commands ###

View File

@@ -1,56 +0,0 @@
"""drop tenancy tables and columns
Revision ID: 9c4f1a2b3d4e
Revises: 8b6d1b8f4b21
Create Date: 2026-02-03 23:35:00.000000
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = "9c4f1a2b3d4e"
down_revision = "8b6d1b8f4b21"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.drop_table("task_subagents")
op.drop_table("task_status_history")
op.drop_table("task_deliverables")
op.drop_table("task_activities")
op.drop_table("transcripts")
op.drop_table("openclaw_sessions")
op.drop_table("workspace_api_tokens")
op.drop_table("orchestration_templates")
op.drop_table("memberships")
op.drop_table("gateway_configs")
op.drop_constraint("tasks_assigned_agent_id_fkey", "tasks", type_="foreignkey")
op.drop_constraint("tasks_org_id_fkey", "tasks", type_="foreignkey")
op.drop_constraint("tasks_workspace_id_fkey", "tasks", type_="foreignkey")
op.drop_index(op.f("ix_tasks_assigned_agent_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_org_id"), table_name="tasks")
op.drop_index(op.f("ix_tasks_workspace_id"), table_name="tasks")
op.drop_column("tasks", "assigned_agent_id")
op.drop_column("tasks", "org_id")
op.drop_column("tasks", "workspace_id")
op.drop_constraint("boards_org_id_fkey", "boards", type_="foreignkey")
op.drop_constraint("boards_workspace_id_fkey", "boards", type_="foreignkey")
op.drop_index(op.f("ix_boards_org_id"), table_name="boards")
op.drop_index(op.f("ix_boards_workspace_id"), table_name="boards")
op.drop_column("boards", "org_id")
op.drop_column("boards", "workspace_id")
op.drop_table("agents")
op.drop_table("workspaces")
op.drop_table("orgs")
def downgrade() -> None:
raise NotImplementedError("Downgrade not supported for simplified tenancy removal.")

View File

@@ -1,70 +0,0 @@
"""add agents and activity events
Revision ID: a1b2c3d4e5f6
Revises: 9c4f1a2b3d4e
Create Date: 2026-02-03 23:50:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f6"
down_revision = "9c4f1a2b3d4e"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"agents",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("last_seen_at", sa.DateTime(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_agents_name"), "agents", ["name"], unique=False)
op.create_index(op.f("ix_agents_status"), "agents", ["status"], unique=False)
op.create_table(
"activity_events",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("event_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("message", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("agent_id", sa.Uuid(), nullable=True),
sa.Column("task_id", sa.Uuid(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]),
sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_activity_events_agent_id"), "activity_events", ["agent_id"], unique=False
)
op.create_index(
op.f("ix_activity_events_event_type"),
"activity_events",
["event_type"],
unique=False,
)
op.create_index(
op.f("ix_activity_events_task_id"), "activity_events", ["task_id"], unique=False
)
def downgrade() -> None:
op.drop_index(op.f("ix_activity_events_task_id"), table_name="activity_events")
op.drop_index(op.f("ix_activity_events_event_type"), table_name="activity_events")
op.drop_index(op.f("ix_activity_events_agent_id"), table_name="activity_events")
op.drop_table("activity_events")
op.drop_index(op.f("ix_agents_status"), table_name="agents")
op.drop_index(op.f("ix_agents_name"), table_name="agents")
op.drop_table("agents")

View File

@@ -1,29 +0,0 @@
"""add task comments index
Revision ID: b9d22e2a4d50
Revises: 8045fbfb157f
Create Date: 2026-02-04 17:32:06.204331
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = 'b9d22e2a4d50'
down_revision = '8045fbfb157f'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"CREATE INDEX IF NOT EXISTS ix_activity_events_task_comment "
"ON activity_events (task_id, created_at) "
"WHERE event_type = 'task.comment'"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_activity_events_task_comment")

View File

@@ -1,31 +0,0 @@
"""add task in_progress_at
Revision ID: c1a2b3c4d5e7
Revises: b9d22e2a4d50
Create Date: 2026-02-04 13:34:25.000000
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = "c1a2b3c4d5e7"
down_revision = "b9d22e2a4d50"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS in_progress_at TIMESTAMP WITHOUT TIME ZONE"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_in_progress_at ON tasks (in_progress_at)"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_tasks_in_progress_at")
op.execute("ALTER TABLE tasks DROP COLUMN IF EXISTS in_progress_at")

View File

@@ -1,38 +0,0 @@
"""add agent openclaw session id
Revision ID: c7f0a2b1d4e3
Revises: a1b2c3d4e5f6
Create Date: 2026-02-04 02:20:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "c7f0a2b1d4e3"
down_revision = "a1b2c3d4e5f6"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"agents",
sa.Column("openclaw_session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)
op.create_index(
op.f("ix_agents_openclaw_session_id"),
"agents",
["openclaw_session_id"],
unique=False,
)
def downgrade() -> None:
op.drop_index(op.f("ix_agents_openclaw_session_id"), table_name="agents")
op.drop_column("agents", "openclaw_session_id")

View File

@@ -1,29 +0,0 @@
"""ensure heartbeat config column
Revision ID: cefef25d4634
Revises: 2b4c2f7b3eda
Create Date: 2026-02-04 16:38:25.234627
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = 'cefef25d4634'
down_revision = '2b4c2f7b3eda'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS heartbeat_config JSON"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS heartbeat_config"
)

View File

@@ -1,38 +0,0 @@
"""add agent token hash
Revision ID: d3e4f5a6b7c8
Revises: c7f0a2b1d4e3
Create Date: 2026-02-04 06:50:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = "d3e4f5a6b7c8"
down_revision = "c7f0a2b1d4e3"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"agents",
sa.Column("agent_token_hash", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
)
op.create_index(
op.f("ix_agents_agent_token_hash"),
"agents",
["agent_token_hash"],
unique=False,
)
def downgrade() -> None:
op.drop_index(op.f("ix_agents_agent_token_hash"), table_name="agents")
op.drop_column("agents", "agent_token_hash")

View File

@@ -1,35 +0,0 @@
"""add agent delete confirmation
Revision ID: e0f28e965fa5
Revises: cefef25d4634
Create Date: 2026-02-04 16:55:33.389505
"""
from __future__ import annotations
from alembic import op
# revision identifiers, used by Alembic.
revision = 'e0f28e965fa5'
down_revision = 'cefef25d4634'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS delete_requested_at TIMESTAMP"
)
op.execute(
"ALTER TABLE agents ADD COLUMN IF NOT EXISTS delete_confirm_token_hash VARCHAR"
)
def downgrade() -> None:
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS delete_confirm_token_hash"
)
op.execute(
"ALTER TABLE agents DROP COLUMN IF EXISTS delete_requested_at"
)

View File

@@ -1,27 +0,0 @@
"""make agent last_seen_at nullable
Revision ID: e4f5a6b7c8d9
Revises: d3e4f5a6b7c8
Create Date: 2026-02-04 07:10:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "e4f5a6b7c8d9"
down_revision = "d3e4f5a6b7c8"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.alter_column("agents", "last_seen_at", existing_type=sa.DateTime(), nullable=True)
def downgrade() -> None:
op.alter_column("agents", "last_seen_at", existing_type=sa.DateTime(), nullable=False)

View File

@@ -1,47 +0,0 @@
"""add board gateway config
Revision ID: f1a2b3c4d5e6
Revises: e4f5a6b7c8d9
Create Date: 2026-02-04 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "f1a2b3c4d5e6"
down_revision = "e4f5a6b7c8d9"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("boards", sa.Column("gateway_url", sa.String(), nullable=True))
op.add_column("boards", sa.Column("gateway_token", sa.String(), nullable=True))
op.add_column(
"boards", sa.Column("gateway_main_session_key", sa.String(), nullable=True)
)
op.add_column(
"boards", sa.Column("gateway_workspace_root", sa.String(), nullable=True)
)
op.add_column("agents", sa.Column("board_id", sa.Uuid(), nullable=True))
op.create_foreign_key(
"agents_board_id_fkey", "agents", "boards", ["board_id"], ["id"]
)
op.create_index(op.f("ix_agents_board_id"), "agents", ["board_id"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_agents_board_id"), table_name="agents")
op.drop_constraint("agents_board_id_fkey", "agents", type_="foreignkey")
op.drop_column("agents", "board_id")
op.drop_column("boards", "gateway_workspace_root")
op.drop_column("boards", "gateway_main_session_key")
op.drop_column("boards", "gateway_token")
op.drop_column("boards", "gateway_url")

View File

@@ -208,7 +208,9 @@ async def create_agent(
)
session.commit()
try:
await send_provisioning_message(agent, board, raw_token, provision_token)
await send_provisioning_message(
agent, board, raw_token, provision_token, auth.user
)
record_activity(
session,
event_type="agent.provision.requested",
@@ -288,7 +290,9 @@ async def update_agent(
session.commit()
session.refresh(agent)
try:
await send_update_message(agent, board, raw_token, provision_token)
await send_update_message(
agent, board, raw_token, provision_token, auth.user
)
record_activity(
session,
event_type="agent.update.requested",
@@ -375,7 +379,9 @@ async def heartbeat_or_create_agent(
)
session.commit()
try:
await send_provisioning_message(agent, board, raw_token, provision_token)
await send_provisioning_message(
agent, board, raw_token, provision_token, actor.user
)
record_activity(
session,
event_type="agent.provision.requested",
@@ -405,7 +411,9 @@ async def heartbeat_or_create_agent(
try:
board = _require_board(session, str(agent.board_id) if agent.board_id else None)
config = _require_gateway_config(board)
await send_provisioning_message(agent, board, raw_token, provision_token)
await send_provisioning_message(
agent, board, raw_token, provision_token, actor.user
)
record_activity(
session,
event_type="agent.provision.requested",

View File

@@ -101,6 +101,10 @@ def create_board(
data = payload.model_dump()
if data.get("gateway_token") == "":
data["gateway_token"] = None
if data.get("identity_template") == "":
data["identity_template"] = None
if data.get("soul_template") == "":
data["soul_template"] = None
if data.get("gateway_url"):
if not data.get("gateway_main_session_key"):
raise HTTPException(
@@ -137,6 +141,10 @@ def update_board(
updates = payload.model_dump(exclude_unset=True)
if updates.get("gateway_token") == "":
updates["gateway_token"] = None
if updates.get("identity_template") == "":
updates["identity_template"] = None
if updates.get("soul_template") == "":
updates["soul_template"] = None
for key, value in updates.items():
setattr(board, key, value)
if board.gateway_url:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import asc, desc
from sqlmodel import Session, col, select
@@ -70,11 +70,26 @@ def has_valid_recent_comment(
@router.get("", response_model=list[TaskRead])
def list_tasks(
status_filter: str | None = Query(default=None, alias="status"),
assigned_agent_id: UUID | None = None,
unassigned: bool | None = None,
limit: int | None = Query(default=None, ge=1, le=200),
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Task]:
return list(session.exec(select(Task).where(Task.board_id == board.id)))
statement = select(Task).where(Task.board_id == board.id)
if status_filter:
statuses = [s.strip() for s in status_filter.split(",") if s.strip()]
if statuses:
statement = statement.where(col(Task.status).in_(statuses))
if assigned_agent_id is not None:
statement = statement.where(col(Task.assigned_agent_id) == assigned_agent_id)
if unassigned:
statement = statement.where(col(Task.assigned_agent_id).is_(None))
if limit is not None:
statement = statement.limit(limit)
return list(session.exec(statement))
@router.post("", response_model=TaskRead)

36
backend/app/api/users.py Normal file
View File

@@ -0,0 +1,36 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session
from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session
from app.models.users import User
from app.schemas.users import UserRead, UserUpdate
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me", response_model=UserRead)
async def get_me(auth: AuthContext = Depends(get_auth_context)) -> UserRead:
if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return UserRead.model_validate(auth.user)
@router.patch("/me", response_model=UserRead)
async def update_me(
payload: UserUpdate,
session: Session = Depends(get_session),
auth: AuthContext = Depends(get_auth_context),
) -> UserRead:
if auth.actor_type != "user" or auth.user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
updates = payload.model_dump(exclude_unset=True)
user: User = auth.user
for key, value in updates.items():
setattr(user, key, value)
session.add(user)
session.commit()
session.refresh(user)
return UserRead.model_validate(user)

View File

@@ -25,5 +25,10 @@ class Settings(BaseSettings):
# Database lifecycle
db_auto_migrate: bool = False
# Logging
log_level: str = "INFO"
log_format: str = "text"
log_use_utc: bool = False
settings = Settings()

View File

@@ -1,14 +1,171 @@
from __future__ import annotations
import json
import logging
import os
import sys
import time
from datetime import datetime, timezone
from typing import Any
from app.core.config import settings
from app.core.version import APP_NAME, APP_VERSION
TRACE_LEVEL = 5
logging.addLevelName(TRACE_LEVEL, "TRACE")
def _trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
if self.isEnabledFor(TRACE_LEVEL):
self._log(TRACE_LEVEL, message, args, **kwargs)
logging.Logger.trace = _trace # type: ignore[attr-defined]
_STANDARD_LOG_RECORD_ATTRS = {
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"message",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
"taskName",
"app",
"version",
}
class AppLogFilter(logging.Filter):
def __init__(self, app_name: str, version: str) -> None:
super().__init__()
self._app_name = app_name
self._version = version
def filter(self, record: logging.LogRecord) -> bool:
record.app = self._app_name
record.version = self._version
return True
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload: dict[str, Any] = {
"timestamp": datetime.fromtimestamp(
record.created, tz=timezone.utc
).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"app": getattr(record, "app", APP_NAME),
"version": getattr(record, "version", APP_VERSION),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
if record.stack_info:
payload["stack"] = self.formatStack(record.stack_info)
for key, value in record.__dict__.items():
if key in _STANDARD_LOG_RECORD_ATTRS or key in payload:
continue
payload[key] = value
return json.dumps(payload, separators=(",", ":"), default=str)
class KeyValueFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
base = super().format(record)
extras = {
key: value
for key, value in record.__dict__.items()
if key not in _STANDARD_LOG_RECORD_ATTRS
}
if not extras:
return base
extra_bits = " ".join(f"{key}={value}" for key, value in extras.items())
return f"{base} {extra_bits}"
class AppLogger:
_configured = False
@classmethod
def _resolve_level(cls) -> tuple[str, int]:
level_name = (settings.log_level or os.getenv("LOG_LEVEL", "INFO")).upper()
if level_name == "TRACE":
return level_name, TRACE_LEVEL
if level_name.isdigit():
return level_name, int(level_name)
return level_name, logging._nameToLevel.get(level_name, logging.INFO)
@classmethod
def configure(cls, *, force: bool = False) -> None:
if cls._configured and not force:
return
level_name, level = cls._resolve_level()
handler = logging.StreamHandler(sys.stdout)
handler.addFilter(AppLogFilter(APP_NAME, APP_VERSION))
format_name = (settings.log_format or "text").lower()
if format_name == "json":
formatter: logging.Formatter = JsonFormatter()
else:
formatter = KeyValueFormatter(
"%(asctime)s %(levelname)s %(name)s %(message)s app=%(app)s version=%(version)s"
)
if settings.log_use_utc:
formatter.converter = time.gmtime
handler.setFormatter(formatter)
root = logging.getLogger()
root.setLevel(level)
root.handlers.clear()
root.addHandler(handler)
# Uvicorn & HTTP clients
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
logging.getLogger(logger_name).setLevel(level)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# SQL logs only at TRACE
sql_loggers = ("sqlalchemy", "sqlalchemy.engine", "sqlalchemy.pool")
if level_name == "TRACE":
for name in sql_loggers:
logger = logging.getLogger(name)
logger.disabled = False
logger.setLevel(logging.INFO)
else:
for name in sql_loggers:
logger = logging.getLogger(name)
logger.disabled = True
cls._configured = True
@classmethod
def get_logger(cls, name: str | None = None) -> logging.Logger:
if not cls._configured:
cls.configure()
return logging.getLogger(name)
def configure_logging() -> None:
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
level = logging._nameToLevel.get(level_name, logging.INFO)
logging.basicConfig(
level=level,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
force=True,
)
AppLogger.configure()

View File

@@ -0,0 +1,2 @@
APP_NAME = "mission-control"
APP_VERSION = "0.1.0"

View File

@@ -9,6 +9,7 @@ from app.api.auth import router as auth_router
from app.api.boards import router as boards_router
from app.api.gateway import router as gateway_router
from app.api.tasks import router as tasks_router
from app.api.users import router as users_router
from app.core.config import settings
from app.core.logging import configure_logging
from app.db.session import init_db
@@ -38,6 +39,16 @@ def health() -> dict[str, bool]:
return {"ok": True}
@app.get("/healthz")
def healthz() -> dict[str, bool]:
return {"ok": True}
@app.get("/readyz")
def readyz() -> dict[str, bool]:
return {"ok": True}
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(auth_router)
api_v1.include_router(agents_router)
@@ -45,4 +56,5 @@ api_v1.include_router(activity_router)
api_v1.include_router(gateway_router)
api_v1.include_router(boards_router)
api_v1.include_router(tasks_router)
api_v1.include_router(users_router)
app.include_router(api_v1)

View File

@@ -18,5 +18,7 @@ class Board(TenantScoped, table=True):
gateway_token: str | None = Field(default=None)
gateway_main_session_key: str | None = Field(default=None)
gateway_workspace_root: str | None = Field(default=None)
identity_template: str | None = Field(default=None)
soul_template: str | None = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -12,4 +12,9 @@ class User(SQLModel, table=True):
clerk_user_id: str = Field(index=True, unique=True)
email: str | None = Field(default=None, index=True)
name: str | None = None
preferred_name: str | None = None
pronouns: str | None = None
timezone: str | None = None
notes: str | None = None
context: str | None = None
is_super_admin: bool = Field(default=False)

View File

@@ -2,7 +2,7 @@ from app.schemas.activity_events import ActivityEventRead
from app.schemas.agents import AgentCreate, AgentRead, AgentUpdate
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
from app.schemas.tasks import TaskCreate, TaskRead, TaskUpdate
from app.schemas.users import UserCreate, UserRead
from app.schemas.users import UserCreate, UserRead, UserUpdate
__all__ = [
"ActivityEventRead",
@@ -17,4 +17,5 @@ __all__ = [
"TaskUpdate",
"UserCreate",
"UserRead",
"UserUpdate",
]

View File

@@ -12,6 +12,8 @@ class BoardBase(SQLModel):
gateway_url: str | None = None
gateway_main_session_key: str | None = None
gateway_workspace_root: str | None = None
identity_template: str | None = None
soul_template: str | None = None
class BoardCreate(BoardBase):
@@ -25,6 +27,8 @@ class BoardUpdate(SQLModel):
gateway_token: str | None = None
gateway_main_session_key: str | None = None
gateway_workspace_root: str | None = None
identity_template: str | None = None
soul_template: str | None = None
class BoardRead(BoardBase):

View File

@@ -9,12 +9,26 @@ class UserBase(SQLModel):
clerk_user_id: str
email: str | None = None
name: str | None = None
preferred_name: str | None = None
pronouns: str | None = None
timezone: str | None = None
notes: str | None = None
context: str | None = None
class UserCreate(UserBase):
pass
class UserUpdate(SQLModel):
name: str | None = None
preferred_name: str | None = None
pronouns: str | None = None
timezone: str | None = None
notes: str | None = None
context: str | None = None
class UserRead(UserBase):
id: UUID
is_super_admin: bool

View File

@@ -12,6 +12,7 @@ from app.core.config import settings
from app.integrations.openclaw_gateway import GatewayConfig, ensure_session, send_message
from app.models.agents import Agent
from app.models.boards import Board
from app.models.users import User
TEMPLATE_FILES = [
"AGENTS.md",
@@ -64,11 +65,18 @@ def _template_env() -> Environment:
)
def _read_templates(context: dict[str, str]) -> dict[str, str]:
def _read_templates(
context: dict[str, str], overrides: dict[str, str] | None = None
) -> dict[str, str]:
env = _template_env()
templates: dict[str, str] = {}
override_map = overrides or {}
for name in TEMPLATE_FILES:
path = _templates_root() / name
override = override_map.get(name)
if override:
templates[name] = env.from_string(override).render(**context).strip()
continue
if not path.exists():
templates[name] = ""
continue
@@ -90,7 +98,9 @@ def _workspace_path(agent_name: str, workspace_root: str) -> str:
return f"{root}/workspace-{_slugify(agent_name)}"
def _build_context(agent: Agent, board: Board, auth_token: str) -> dict[str, str]:
def _build_context(
agent: Agent, board: Board, auth_token: str, user: User | None
) -> dict[str, str]:
if not board.gateway_workspace_root:
raise ValueError("gateway_workspace_root is required")
if not board.gateway_main_session_key:
@@ -111,25 +121,32 @@ def _build_context(agent: Agent, board: Board, auth_token: str) -> dict[str, str
"auth_token": auth_token,
"main_session_key": main_session_key,
"workspace_root": workspace_root,
"user_name": "Unset",
"user_preferred_name": "Unset",
"user_timezone": "Unset",
"user_notes": "Fill in user context.",
"user_name": user.name if user else "",
"user_preferred_name": user.preferred_name if user else "",
"user_pronouns": user.pronouns if user else "",
"user_timezone": user.timezone if user else "",
"user_notes": user.notes if user else "",
"user_context": user.context if user else "",
}
def _build_file_blocks(context: dict[str, str]) -> str:
templates = _read_templates(context)
def _build_file_blocks(context: dict[str, str], board: Board) -> str:
overrides: dict[str, str] = {}
if board.identity_template:
overrides["IDENTITY.md"] = board.identity_template
if board.soul_template:
overrides["SOUL.md"] = board.soul_template
templates = _read_templates(context, overrides=overrides)
return "".join(
_render_file_block(name, templates.get(name, "")) for name in TEMPLATE_FILES
)
def build_provisioning_message(
agent: Agent, board: Board, auth_token: str, confirm_token: str
agent: Agent, board: Board, auth_token: str, confirm_token: str, user: User | None
) -> str:
context = _build_context(agent, board, auth_token)
file_blocks = _build_file_blocks(context)
context = _build_context(agent, board, auth_token, user)
file_blocks = _build_file_blocks(context, board)
heartbeat_snippet = json.dumps(
{
"id": _agent_key(agent),
@@ -173,10 +190,10 @@ def build_provisioning_message(
def build_update_message(
agent: Agent, board: Board, auth_token: str, confirm_token: str
agent: Agent, board: Board, auth_token: str, confirm_token: str, user: User | None
) -> str:
context = _build_context(agent, board, auth_token)
file_blocks = _build_file_blocks(context)
context = _build_context(agent, board, auth_token, user)
file_blocks = _build_file_blocks(context, board)
heartbeat_snippet = json.dumps(
{
"id": _agent_key(agent),
@@ -223,6 +240,7 @@ async def send_provisioning_message(
board: Board,
auth_token: str,
confirm_token: str,
user: User | None,
) -> None:
if not board.gateway_url:
return
@@ -231,7 +249,7 @@ async def send_provisioning_message(
main_session = board.gateway_main_session_key
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
await ensure_session(main_session, config=config, label="Main Agent")
message = build_provisioning_message(agent, board, auth_token, confirm_token)
message = build_provisioning_message(agent, board, auth_token, confirm_token, user)
await send_message(message, session_key=main_session, config=config, deliver=False)
@@ -240,6 +258,7 @@ async def send_update_message(
board: Board,
auth_token: str,
confirm_token: str,
user: User | None,
) -> None:
if not board.gateway_url:
return
@@ -248,5 +267,5 @@ async def send_update_message(
main_session = board.gateway_main_session_key
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
await ensure_session(main_session, config=config, label="Main Agent")
message = build_update_message(agent, board, auth_token, confirm_token)
message = build_update_message(agent, board, auth_token, confirm_token, user)
await send_message(message, session_key=main_session, config=config, deliver=False)