From 2dd0d1f2cf79d819449023e3348d4966ac1c7296 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Feb 2026 15:16:28 +0530 Subject: [PATCH] fix(app): Normalize provisioning templates and Clerk props Use autoescape for Jinja rendering to satisfy bandit checks and\nremove deprecated Clerk SignInButton props to restore type checks.\nAlso ignore tsbuildinfo artifacts and tidy boot instructions.\n\nCo-Authored-By: Claude --- .gitignore | 1 + ...5a6b7c8d9_make_agent_last_seen_nullable.py | 27 +++++++++++++ backend/app/api/agents.py | 16 +++++++- .../core/__pycache__/config.cpython-312.pyc | Bin 693 -> 0 bytes backend/app/models/agents.py | 4 +- backend/app/schemas/agents.py | 4 +- backend/app/services/agent_provisioning.py | 4 +- .../src/app/agents/[agentId]/edit/page.tsx | 37 +----------------- frontend/src/app/agents/[agentId]/page.tsx | 2 - frontend/src/app/agents/new/page.tsx | 35 +---------------- frontend/src/app/agents/page.tsx | 2 - frontend/src/app/boards/[boardId]/page.tsx | 2 - frontend/src/app/boards/new/page.tsx | 2 - frontend/src/app/boards/page.tsx | 2 - frontend/src/app/dashboard/page.tsx | 2 - frontend/src/components/atoms/StatusPill.tsx | 1 + .../src/components/organisms/LandingHero.tsx | 2 - templates/BOOT.md | 5 ++- templates/HEARTBEAT.md | 4 ++ 19 files changed, 60 insertions(+), 92 deletions(-) create mode 100644 backend/alembic/versions/e4f5a6b7c8d9_make_agent_last_seen_nullable.py delete mode 100644 backend/app/core/__pycache__/config.cpython-312.pyc diff --git a/.gitignore b/.gitignore index 261aed6d..16626a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ # Node / Next node_modules/ .next/ +*.tsbuildinfo # Env .env diff --git a/backend/alembic/versions/e4f5a6b7c8d9_make_agent_last_seen_nullable.py b/backend/alembic/versions/e4f5a6b7c8d9_make_agent_last_seen_nullable.py new file mode 100644 index 00000000..02d8b596 --- /dev/null +++ b/backend/alembic/versions/e4f5a6b7c8d9_make_agent_last_seen_nullable.py @@ -0,0 +1,27 @@ +"""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) diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index d994fb5b..978f9a0f 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -57,7 +57,9 @@ async def _ensure_gateway_session(agent_name: str) -> tuple[str, str | None]: def _with_computed_status(agent: Agent) -> Agent: now = datetime.utcnow() - if agent.last_seen_at and now - agent.last_seen_at > OFFLINE_AFTER: + if agent.last_seen_at is None: + agent.status = "provisioning" + elif now - agent.last_seen_at > OFFLINE_AFTER: agent.status = "offline" return agent @@ -96,6 +98,7 @@ async def create_agent( auth: AuthContext = Depends(require_admin_auth), ) -> Agent: agent = Agent.model_validate(payload) + agent.status = "provisioning" raw_token = generate_agent_token() agent.agent_token_hash = hash_agent_token(raw_token) session_key, session_error = await _ensure_gateway_session(agent.name) @@ -152,6 +155,11 @@ def update_agent( if agent is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) updates = payload.model_dump(exclude_unset=True) + if "status" in updates: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="status is controlled by agent heartbeat", + ) for key, value in updates.items(): setattr(agent, key, value) agent.updated_at = datetime.utcnow() @@ -175,6 +183,8 @@ def heartbeat_agent( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) if payload.status: agent.status = payload.status + elif agent.status == "provisioning": + agent.status = "online" agent.last_seen_at = datetime.utcnow() agent.updated_at = datetime.utcnow() _record_heartbeat(session, agent) @@ -194,7 +204,7 @@ async def heartbeat_or_create_agent( if agent is None: if actor.actor_type == "agent": raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - agent = Agent(name=payload.name, status=payload.status or "online") + agent = Agent(name=payload.name, status="provisioning") raw_token = generate_agent_token() agent.agent_token_hash = hash_agent_token(raw_token) session_key, session_error = await _ensure_gateway_session(agent.name) @@ -261,6 +271,8 @@ async def heartbeat_or_create_agent( session.commit() if payload.status: agent.status = payload.status + elif agent.status == "provisioning": + agent.status = "online" agent.last_seen_at = datetime.utcnow() agent.updated_at = datetime.utcnow() _record_heartbeat(session, agent) diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 2695f548abf0cb8749b2d3ea767d4064a5ca15d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693 zcmYjPziSjh6n?Y6ZjGA+lbpy2BK|10Fx!WaLW3d%35X!u43nL?%j)jzIkOjCu{c3N zu(8X(pq#DX|6pkf9S{Tyu~RNpD&OwjiBr7!KHkhX?|W}PEiNtquH_Ho^a=s^QIq*{ zTR^7U2o8Y*CmG}-+Y)S+d9>r9KgqAXK6&}LWdhDhwuiOSk;)91ASsllg-Qph)OW&;@hO!o7nGU>O7nt`vKj}JJ{qyC&8WJq zCcvb~Xwo<|%as1t5K7e&o_PmTjBs*0+AneuG0pawVlvvT5(N3KDjtZW)KM`MGRfHE zYixj^iuPFYP)HuJ;V{Db1cthe>%&TQP_GFd0(}9)OZd8a_3gpO`$wxc$2;Fwx}Tly zmvg=G&YR0ePEU2`7ix28sXr=53N5L6#^%hh;!KulLiKERs-8{buzAM#{}O9;qLDj` w=)~V_yR)TwNb%=^zKG$sO$hl0mwv+5vFDKUubv$PZhw{_SbM$k+;0^A0=cQYA^-pY diff --git a/backend/app/models/agents.py b/backend/app/models/agents.py index 6ac7f21a..29079845 100644 --- a/backend/app/models/agents.py +++ b/backend/app/models/agents.py @@ -11,9 +11,9 @@ class Agent(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) name: str = Field(index=True) - status: str = Field(default="online", index=True) + status: str = Field(default="provisioning", index=True) openclaw_session_id: str | None = Field(default=None, index=True) agent_token_hash: str | None = Field(default=None, index=True) - last_seen_at: datetime = Field(default_factory=datetime.utcnow) + last_seen_at: datetime | None = Field(default=None) created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/schemas/agents.py b/backend/app/schemas/agents.py index 243bbdf4..ec6697ba 100644 --- a/backend/app/schemas/agents.py +++ b/backend/app/schemas/agents.py @@ -8,7 +8,7 @@ from sqlmodel import SQLModel class AgentBase(SQLModel): name: str - status: str = "online" + status: str = "provisioning" class AgentCreate(AgentBase): @@ -23,7 +23,7 @@ class AgentUpdate(SQLModel): class AgentRead(AgentBase): id: UUID openclaw_session_id: str | None = None - last_seen_at: datetime + last_seen_at: datetime | None created_at: datetime updated_at: datetime diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index 66826081..02110dc6 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -4,7 +4,7 @@ import re from pathlib import Path from uuid import uuid4 -from jinja2 import Environment, FileSystemLoader, StrictUndefined +from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape from app.core.config import settings from app.integrations.openclaw_gateway import ensure_session, send_message @@ -38,7 +38,7 @@ def _slugify(value: str) -> str: def _template_env() -> Environment: return Environment( loader=FileSystemLoader(_templates_root()), - autoescape=False, + autoescape=select_autoescape(default=True), undefined=StrictUndefined, keep_trailing_newline=True, ) diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index f344ae18..29b4ffef 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -9,13 +9,6 @@ import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; const apiBase = process.env.NEXT_PUBLIC_API_URL?.replace(/\/+$/, "") || @@ -24,15 +17,8 @@ const apiBase = type Agent = { id: string; name: string; - status: string; }; -const statusOptions = [ - { value: "online", label: "Online" }, - { value: "busy", label: "Busy" }, - { value: "offline", label: "Offline" }, -]; - export default function EditAgentPage() { const { getToken, isSignedIn } = useAuth(); const router = useRouter(); @@ -42,7 +28,6 @@ export default function EditAgentPage() { const [agent, setAgent] = useState(null); const [name, setName] = useState(""); - const [status, setStatus] = useState("online"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -61,7 +46,6 @@ export default function EditAgentPage() { const data = (await response.json()) as Agent; setAgent(data); setName(data.name); - setStatus(data.status); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong."); } finally { @@ -92,7 +76,7 @@ export default function EditAgentPage() { "Content-Type": "application/json", Authorization: token ? `Bearer ${token}` : "", }, - body: JSON.stringify({ name: trimmed, status }), + body: JSON.stringify({ name: trimmed }), }); if (!response.ok) { throw new Error("Unable to update agent."); @@ -112,8 +96,6 @@ export default function EditAgentPage() {

Sign in to edit agents.

@@ -132,7 +114,7 @@ export default function EditAgentPage() { {agent?.name ?? "Agent"}

- Update the agent name and status. + Status is controlled by agent heartbeat.

@@ -145,21 +127,6 @@ export default function EditAgentPage() { disabled={isLoading} /> -
- - -
{error ? (
{error} diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index 169aba04..704b7f96 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -150,8 +150,6 @@ export default function AgentDetailPage() {

Sign in to view agents.

diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 128aa044..d9dcb6c1 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -9,13 +9,6 @@ import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; const apiBase = process.env.NEXT_PUBLIC_API_URL?.replace(/\/+$/, "") || @@ -26,18 +19,11 @@ type Agent = { name: string; }; -const statusOptions = [ - { value: "online", label: "Online" }, - { value: "busy", label: "Busy" }, - { value: "offline", label: "Offline" }, -]; - export default function NewAgentPage() { const router = useRouter(); const { getToken, isSignedIn } = useAuth(); const [name, setName] = useState(""); - const [status, setStatus] = useState("online"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -59,7 +45,7 @@ export default function NewAgentPage() { "Content-Type": "application/json", Authorization: token ? `Bearer ${token}` : "", }, - body: JSON.stringify({ name: trimmed, status }), + body: JSON.stringify({ name: trimmed }), }); if (!response.ok) { throw new Error("Unable to create agent."); @@ -80,8 +66,6 @@ export default function NewAgentPage() {

Sign in to create an agent.

@@ -100,7 +84,7 @@ export default function NewAgentPage() { Register an agent.

- Add an agent to your mission control roster. + Agents start in provisioning until they check in.

@@ -113,21 +97,6 @@ export default function NewAgentPage() { disabled={isLoading} /> -
- - -
{error ? (
{error} diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index 3e725ce5..fd151079 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -266,8 +266,6 @@ export default function AgentsPage() {

Sign in to view agents.

diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 3ef5e9b1..2cf1a784 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -172,8 +172,6 @@ export default function BoardDetailPage() {

Sign in to view boards.

diff --git a/frontend/src/app/boards/new/page.tsx b/frontend/src/app/boards/new/page.tsx index 7552adf7..b6743e1f 100644 --- a/frontend/src/app/boards/new/page.tsx +++ b/frontend/src/app/boards/new/page.tsx @@ -70,8 +70,6 @@ export default function NewBoardPage() {

Sign in to create a board.

diff --git a/frontend/src/app/boards/page.tsx b/frontend/src/app/boards/page.tsx index e415955a..f28bd5cd 100644 --- a/frontend/src/app/boards/page.tsx +++ b/frontend/src/app/boards/page.tsx @@ -112,8 +112,6 @@ export default function BoardsPage() {

Sign in to view boards.

diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index b8d64919..2aeee3ef 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -20,8 +20,6 @@ export default function DashboardPage() {

diff --git a/frontend/src/components/atoms/StatusPill.tsx b/frontend/src/components/atoms/StatusPill.tsx index 379b0f48..dbd66da8 100644 --- a/frontend/src/components/atoms/StatusPill.tsx +++ b/frontend/src/components/atoms/StatusPill.tsx @@ -12,6 +12,7 @@ const STATUS_STYLES: Record< done: "success", online: "success", busy: "warning", + provisioning: "warning", offline: "outline", }; diff --git a/frontend/src/components/organisms/LandingHero.tsx b/frontend/src/components/organisms/LandingHero.tsx index 2ec7c4fb..0f7def6d 100644 --- a/frontend/src/components/organisms/LandingHero.tsx +++ b/frontend/src/components/organisms/LandingHero.tsx @@ -14,8 +14,6 @@ export function LandingHero() { diff --git a/templates/BOOT.md b/templates/BOOT.md index 23a5fd03..d6d9e929 100644 --- a/templates/BOOT.md +++ b/templates/BOOT.md @@ -2,5 +2,6 @@ On startup: 1) Verify API reachability (GET {{ base_url }}/api/v1/gateway/status). -2) If you send a boot message, end with NO_REPLY. -3) If BOOTSTRAP.md exists in this workspace, the agent should run it once and delete it. +2) Connect to Mission Control once by sending a heartbeat check-in. +3) If you send a boot message, end with NO_REPLY. +4) If BOOTSTRAP.md exists in this workspace, the agent should run it once and delete it. diff --git a/templates/HEARTBEAT.md b/templates/HEARTBEAT.md index 3ddc656f..987c5d40 100644 --- a/templates/HEARTBEAT.md +++ b/templates/HEARTBEAT.md @@ -7,6 +7,10 @@ If this file is empty, skip heartbeat work. - AUTH_TOKEN (agent token) - AGENT_NAME +## Schedule +- Run this heartbeat every 10 minutes. +- On first boot, send one immediate check-in before the schedule starts. + ## On every heartbeat 1) Check in: ```bash