From 9bd34503d6b81360579eac6464f4224ee6feaada Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Tue, 10 Feb 2026 20:01:08 +0530 Subject: [PATCH] refactor: reorganize template files and update provisioning paths --- backend/Dockerfile | 6 +- backend/app/services/openclaw/provisioning.py | 18 ++- {templates => backend/templates}/AGENTS.md | 0 {templates => backend/templates}/AUTONOMY.md | 0 {templates => backend/templates}/BOOT.md | 0 {templates => backend/templates}/BOOTSTRAP.md | 0 .../templates}/HEARTBEAT_AGENT.md | 0 .../templates}/HEARTBEAT_LEAD.md | 0 {templates => backend/templates}/IDENTITY.md | 0 .../templates}/MAIN_AGENTS.md | 0 {templates => backend/templates}/MAIN_BOOT.md | 0 .../templates}/MAIN_HEARTBEAT.md | 0 .../templates}/MAIN_TOOLS.md | 0 {templates => backend/templates}/MAIN_USER.md | 0 {templates => backend/templates}/MEMORY.md | 0 {templates => backend/templates}/SELF.md | 0 {templates => backend/templates}/SOUL.md | 0 {templates => backend/templates}/TASK_SOUL.md | 0 {templates => backend/templates}/TOOLS.md | 0 {templates => backend/templates}/USER.md | 0 .../tests/test_agent_provisioning_utils.py | 104 ++++++++++++++++++ compose.yml | 2 +- docs/architecture/README.md | 2 +- 23 files changed, 125 insertions(+), 7 deletions(-) rename {templates => backend/templates}/AGENTS.md (100%) rename {templates => backend/templates}/AUTONOMY.md (100%) rename {templates => backend/templates}/BOOT.md (100%) rename {templates => backend/templates}/BOOTSTRAP.md (100%) rename {templates => backend/templates}/HEARTBEAT_AGENT.md (100%) rename {templates => backend/templates}/HEARTBEAT_LEAD.md (100%) rename {templates => backend/templates}/IDENTITY.md (100%) rename {templates => backend/templates}/MAIN_AGENTS.md (100%) rename {templates => backend/templates}/MAIN_BOOT.md (100%) rename {templates => backend/templates}/MAIN_HEARTBEAT.md (100%) rename {templates => backend/templates}/MAIN_TOOLS.md (100%) rename {templates => backend/templates}/MAIN_USER.md (100%) rename {templates => backend/templates}/MEMORY.md (100%) rename {templates => backend/templates}/SELF.md (100%) rename {templates => backend/templates}/SOUL.md (100%) rename {templates => backend/templates}/TASK_SOUL.md (100%) rename {templates => backend/templates}/TOOLS.md (100%) rename {templates => backend/templates}/USER.md (100%) diff --git a/backend/Dockerfile b/backend/Dockerfile index 2d88e81a..18c7e478 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -38,9 +38,9 @@ COPY backend/migrations ./migrations COPY backend/alembic.ini ./alembic.ini COPY backend/app ./app -# Copy repo-level templates used by agent provisioning, etc. -# (backend code resolves these from repo root) -COPY templates ./templates +# Copy provisioning templates. +# In-repo these live at `backend/templates/`; runtime path is `/app/templates`. +COPY backend/templates ./templates # Default API port EXPOSE 8000 diff --git a/backend/app/services/openclaw/provisioning.py b/backend/app/services/openclaw/provisioning.py index 1f939ba3..957b5c84 100644 --- a/backend/app/services/openclaw/provisioning.py +++ b/backend/app/services/openclaw/provisioning.py @@ -544,9 +544,16 @@ class OpenClawGatewayControlPlane(GatewayControlPlane): if not isinstance(item, dict): continue name = item.get("name") + if not isinstance(name, str) or not name: + name = item.get("path") if isinstance(name, str) and name: supported.add(name) - return supported or set(DEFAULT_GATEWAY_FILES) + + # Always include Mission Control's expected template files even if the gateway's default + # agent reports a different file set (e.g. `prompts/system.md`). This prevents provisioning + # from silently skipping our templates due to gateway-side defaults or version skew. + supported.update(DEFAULT_GATEWAY_FILES) + return supported async def list_agent_files(self, agent_id: str) -> dict[str, dict[str, Any]]: payload = await openclaw_call( @@ -564,6 +571,8 @@ class OpenClawGatewayControlPlane(GatewayControlPlane): if not isinstance(item, dict): continue name = item.get("name") + if not isinstance(name, str) or not name: + name = item.get("path") if isinstance(name, str) and name: index[name] = dict(item) return index @@ -686,11 +695,15 @@ class BaseAgentLifecycleManager(ABC): agent_id: str, rendered: dict[str, str], existing_files: dict[str, dict[str, Any]], + action: str, ) -> None: for name, content in rendered.items(): if content == "": continue - if name in PRESERVE_AGENT_EDITABLE_FILES: + # Preserve "editable" files only during updates. During first-time provisioning, + # the gateway may pre-create defaults for USER/SELF/etc, and we still want to + # apply Mission Control's templates. + if action == "update" and name in PRESERVE_AGENT_EDITABLE_FILES: entry = existing_files.get(name) if entry and not bool(entry.get("missing")): continue @@ -764,6 +777,7 @@ class BaseAgentLifecycleManager(ABC): agent_id=agent_id, rendered=rendered, existing_files=existing_files, + action=options.action, ) if options.reset_session: await self._control_plane.reset_agent_session(session_key) diff --git a/templates/AGENTS.md b/backend/templates/AGENTS.md similarity index 100% rename from templates/AGENTS.md rename to backend/templates/AGENTS.md diff --git a/templates/AUTONOMY.md b/backend/templates/AUTONOMY.md similarity index 100% rename from templates/AUTONOMY.md rename to backend/templates/AUTONOMY.md diff --git a/templates/BOOT.md b/backend/templates/BOOT.md similarity index 100% rename from templates/BOOT.md rename to backend/templates/BOOT.md diff --git a/templates/BOOTSTRAP.md b/backend/templates/BOOTSTRAP.md similarity index 100% rename from templates/BOOTSTRAP.md rename to backend/templates/BOOTSTRAP.md diff --git a/templates/HEARTBEAT_AGENT.md b/backend/templates/HEARTBEAT_AGENT.md similarity index 100% rename from templates/HEARTBEAT_AGENT.md rename to backend/templates/HEARTBEAT_AGENT.md diff --git a/templates/HEARTBEAT_LEAD.md b/backend/templates/HEARTBEAT_LEAD.md similarity index 100% rename from templates/HEARTBEAT_LEAD.md rename to backend/templates/HEARTBEAT_LEAD.md diff --git a/templates/IDENTITY.md b/backend/templates/IDENTITY.md similarity index 100% rename from templates/IDENTITY.md rename to backend/templates/IDENTITY.md diff --git a/templates/MAIN_AGENTS.md b/backend/templates/MAIN_AGENTS.md similarity index 100% rename from templates/MAIN_AGENTS.md rename to backend/templates/MAIN_AGENTS.md diff --git a/templates/MAIN_BOOT.md b/backend/templates/MAIN_BOOT.md similarity index 100% rename from templates/MAIN_BOOT.md rename to backend/templates/MAIN_BOOT.md diff --git a/templates/MAIN_HEARTBEAT.md b/backend/templates/MAIN_HEARTBEAT.md similarity index 100% rename from templates/MAIN_HEARTBEAT.md rename to backend/templates/MAIN_HEARTBEAT.md diff --git a/templates/MAIN_TOOLS.md b/backend/templates/MAIN_TOOLS.md similarity index 100% rename from templates/MAIN_TOOLS.md rename to backend/templates/MAIN_TOOLS.md diff --git a/templates/MAIN_USER.md b/backend/templates/MAIN_USER.md similarity index 100% rename from templates/MAIN_USER.md rename to backend/templates/MAIN_USER.md diff --git a/templates/MEMORY.md b/backend/templates/MEMORY.md similarity index 100% rename from templates/MEMORY.md rename to backend/templates/MEMORY.md diff --git a/templates/SELF.md b/backend/templates/SELF.md similarity index 100% rename from templates/SELF.md rename to backend/templates/SELF.md diff --git a/templates/SOUL.md b/backend/templates/SOUL.md similarity index 100% rename from templates/SOUL.md rename to backend/templates/SOUL.md diff --git a/templates/TASK_SOUL.md b/backend/templates/TASK_SOUL.md similarity index 100% rename from templates/TASK_SOUL.md rename to backend/templates/TASK_SOUL.md diff --git a/templates/TOOLS.md b/backend/templates/TOOLS.md similarity index 100% rename from templates/TOOLS.md rename to backend/templates/TOOLS.md diff --git a/templates/USER.md b/backend/templates/USER.md similarity index 100% rename from templates/USER.md rename to backend/templates/USER.md diff --git a/backend/tests/test_agent_provisioning_utils.py b/backend/tests/test_agent_provisioning_utils.py index ac7e1e50..debccddb 100644 --- a/backend/tests/test_agent_provisioning_utils.py +++ b/backend/tests/test_agent_provisioning_utils.py @@ -81,6 +81,13 @@ def test_agent_lifecycle_workspace_path_preserves_tilde_in_workspace_root(): ) +def test_templates_root_points_to_repo_templates_dir(): + root = agent_provisioning._templates_root() + assert root.name == "templates" + assert root.parent.name == "backend" + assert (root / "AGENTS.md").exists() + + @dataclass class _GatewayStub: id: UUID @@ -164,3 +171,100 @@ async def test_provision_main_agent_uses_dedicated_openclaw_agent_id(monkeypatch expected_agent_id = GatewayAgentIdentity.openclaw_agent_id_for_id(gateway_id) assert captured["patched_agent_id"] == expected_agent_id assert captured["files_index_agent_id"] == expected_agent_id + + +@pytest.mark.asyncio +async def test_list_supported_files_always_includes_default_gateway_files(monkeypatch): + """Provisioning should not depend solely on whatever the gateway's default agent reports.""" + + async def _fake_openclaw_call(method, params=None, config=None): + _ = config + if method == "agents.list": + return {"defaultId": "main"} + if method == "agents.files.list": + assert params == {"agentId": "main"} + return {"files": [{"path": "prompts/system.md", "missing": True}]} + raise AssertionError(f"Unexpected method: {method}") + + monkeypatch.setattr(agent_provisioning, "openclaw_call", _fake_openclaw_call) + cp = agent_provisioning.OpenClawGatewayControlPlane( + agent_provisioning.GatewayClientConfig(url="ws://gateway.example/ws", token=None), + ) + supported = await cp.list_supported_files() + + # Newer gateways may surface other file paths, but we still must include our templates. + assert "prompts/system.md" in supported + for required in agent_provisioning.DEFAULT_GATEWAY_FILES: + assert required in supported + + +@pytest.mark.asyncio +async def test_provision_overwrites_user_md_on_first_provision(monkeypatch): + """Gateway may pre-create USER.md; we still want MC's template on first provision.""" + + class _ControlPlaneStub: + def __init__(self): + self.writes: list[tuple[str, str]] = [] + + async def ensure_agent_session(self, session_key, *, label=None): + return None + + async def reset_agent_session(self, session_key): + return None + + async def delete_agent_session(self, session_key): + return None + + async def upsert_agent(self, registration): + return None + + async def delete_agent(self, agent_id, *, delete_files=True): + return None + + async def list_supported_files(self): + # Minimal set. + return {"USER.md"} + + async def list_agent_files(self, agent_id): + # Pretend gateway created USER.md already. + return {"USER.md": {"name": "USER.md", "missing": False}} + + async def set_agent_file(self, *, agent_id, name, content): + self.writes.append((name, content)) + + async def patch_agent_heartbeats(self, entries): + return None + + @dataclass + class _GatewayTiny: + id: UUID + name: str + url: str + token: str | None + workspace_root: str + + class _Manager(agent_provisioning.BaseAgentLifecycleManager): + def _agent_id(self, agent): + return "agent-x" + + def _build_context(self, *, agent, auth_token, user, board): + return {} + + gateway = _GatewayTiny( + id=uuid4(), + name="G", + url="ws://x", + token=None, + workspace_root="/tmp", + ) + cp = _ControlPlaneStub() + mgr = _Manager(gateway, cp) # type: ignore[arg-type] + + # Rendered content is non-empty; action is "provision" so we should overwrite. + await mgr._set_agent_files( + agent_id="agent-x", + rendered={"USER.md": "from-mc"}, + existing_files={"USER.md": {"name": "USER.md", "missing": False}}, + action="provision", + ) + assert ("USER.md", "from-mc") in cp.writes diff --git a/compose.yml b/compose.yml index 749b80e0..4d046640 100644 --- a/compose.yml +++ b/compose.yml @@ -20,7 +20,7 @@ services: backend: build: # Build from repo root so the backend image can include repo-level assets - # like `templates/`. + # like `backend/templates/`. context: . dockerfile: backend/Dockerfile env_file: diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 13bf99bb..8b3f3bde 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -81,7 +81,7 @@ There is currently no queue runtime configured in this repo. Repo root: - `compose.yml` — local/self-host stack - `.env.example` — compose/local defaults -- `templates/` — shared templates +- `backend/templates/` — shared templates Backend: - `backend/app/api/` — REST routers