diff --git a/backend/alembic/versions/1c2d3e4f5a6b_add_openclaw_agent_id_to_employees.py b/backend/alembic/versions/1c2d3e4f5a6b_add_openclaw_agent_id_to_employees.py new file mode 100644 index 0000000..ab10a1e --- /dev/null +++ b/backend/alembic/versions/1c2d3e4f5a6b_add_openclaw_agent_id_to_employees.py @@ -0,0 +1,26 @@ +"""Add openclaw_agent_id to employees + +Revision ID: 1c2d3e4f5a6b +Revises: 0a1b2c3d4e5f +Create Date: 2026-02-02 + +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "1c2d3e4f5a6b" +down_revision = "0a1b2c3d4e5f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("employees", sa.Column("openclaw_agent_id", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("employees", "openclaw_agent_id") diff --git a/backend/app/api/org.py b/backend/app/api/org.py index 4444d5a..29dbcca 100644 --- a/backend/app/api/org.py +++ b/backend/app/api/org.py @@ -96,8 +96,6 @@ def _maybe_auto_provision_agent(session: Session, *, emp: Employee, actor_employ return if emp.status != "active": return - if not emp.notify_enabled: - return if emp.openclaw_session_key: return @@ -105,6 +103,30 @@ def _maybe_auto_provision_agent(session: Session, *, emp: Employee, actor_employ if client is None: return + # FULL IMPLEMENTATION: ensure a dedicated OpenClaw agent profile exists per employee. + try: + from app.integrations.openclaw_agents import ensure_full_agent_profile + + info = ensure_full_agent_profile( + client=client, + employee_id=int(emp.id), + employee_name=emp.name, + ) + emp.openclaw_agent_id = info["agent_id"] + session.add(emp) + session.flush() + except Exception as e: + log_activity( + session, + actor_employee_id=actor_employee_id, + entity_type="employee", + entity_id=emp.id, + verb="agent_profile_failed", + payload={"error": f"{type(e).__name__}: {e}"}, + ) + # Do not block employee creation on provisioning. + return + label = f"employee:{emp.id}:{emp.name}" try: resp = client.tools_invoke( @@ -112,7 +134,7 @@ def _maybe_auto_provision_agent(session: Session, *, emp: Employee, actor_employ { "task": _default_agent_prompt(emp), "label": label, - "agentId": "main", + "agentId": emp.openclaw_agent_id, "cleanup": "keep", "runTimeoutSeconds": 600, }, diff --git a/backend/app/integrations/openclaw_agents.py b/backend/app/integrations/openclaw_agents.py new file mode 100644 index 0000000..6ffb5ca --- /dev/null +++ b/backend/app/integrations/openclaw_agents.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +import re +import time +from typing import Any + +from app.integrations.openclaw import OpenClawClient + + +def _slug(s: str) -> str: + s = (s or "").strip().lower() + s = re.sub(r"[^a-z0-9]+", "-", s) + s = re.sub(r"-+", "-", s).strip("-") + return s or "agent" + + +def desired_agent_id(*, employee_id: int, name: str) -> str: + return f"employee-{employee_id}-{_slug(name)}" + + +def ensure_full_agent_profile( + *, + client: OpenClawClient, + employee_id: int, + employee_name: str, +) -> dict[str, str]: + """Ensure an OpenClaw agent profile exists for this employee. + + Returns {"agent_id": ..., "workspace": ...}. + + Implementation strategy: + - Create per-agent workspace + agent dir on the gateway host. + - Add/ensure entry in openclaw.json agents.list. + + NOTE: This uses OpenClaw gateway tools via /tools/invoke (gateway + exec). + """ + + agent_id = desired_agent_id(employee_id=employee_id, name=employee_name) + + workspace = f"/home/asaharan/.openclaw/workspaces/{agent_id}" + agent_dir = f"/home/asaharan/.openclaw/agents/{agent_id}/agent" + + # 1) Create dirs + client.tools_invoke( + "exec", + { + "command": f"mkdir -p {workspace} {agent_dir}", + }, + timeout_s=20.0, + ) + + # 2) Write minimal identity files in the per-agent workspace + identity_md = ( + "# IDENTITY.md\n\n" + "- **Name:** " + employee_name + "\n" + "- **Creature:** AI agent employee (Mission Control)\n" + "- **Vibe:** Direct, action-oriented, leaves audit trails\n" + ) + user_md = ( + "# USER.md\n\n" + "You work for Abhimanyu.\n" + "You must execute Mission Control tasks via the API and keep state synced.\n" + ) + + # Use cat heredocs to avoid dependency on extra tooling. + client.tools_invoke( + "exec", + { + "command": "bash -lc " + + json.dumps( + """ +cat > {ws}/IDENTITY.md <<'EOF' +{identity} +EOF +cat > {ws}/USER.md <<'EOF' +{user} +EOF +""".format(ws=workspace, identity=identity_md, user=user_md) + ), + }, + timeout_s=20.0, + ) + + # 3) Update openclaw.json agents.list (idempotent) + cfg_resp = client.tools_invoke("gateway", {"action": "config.get"}, timeout_s=20.0) + raw = ( + (((cfg_resp or {}).get("result") or {}).get("content") or [{}])[0].get("text") + if isinstance((((cfg_resp or {}).get("result") or {}).get("content") or [{}]), list) + else None + ) + + if not raw: + # fallback: tool may return {ok:true,result:{raw:...}} + raw = ((cfg_resp.get("result") or {}).get("raw")) if isinstance(cfg_resp, dict) else None + + if not raw: + raise RuntimeError("Unable to read gateway config via tools") + + cfg = json.loads(raw) + + agents = cfg.get("agents") or {} + agents_list = agents.get("list") or [] + if not isinstance(agents_list, list): + agents_list = [] + + exists = any(isinstance(a, dict) and a.get("id") == agent_id for a in agents_list) + if not exists: + agents_list.append( + { + "id": agent_id, + "name": employee_name, + "workspace": workspace, + "agentDir": agent_dir, + "identity": {"name": employee_name, "emoji": "🜁"}, + } + ) + agents["list"] = agents_list + cfg["agents"] = agents + + client.tools_invoke( + "gateway", + {"action": "config.apply", "raw": json.dumps(cfg)}, + timeout_s=30.0, + ) + # give the gateway a moment to reload the agent registry + time.sleep(2.5) + + return {"agent_id": agent_id, "workspace": workspace}