refactor: add overwrite option to various services and update documentation
This commit is contained in:
2
Makefile
2
Makefile
@@ -147,7 +147,7 @@ rq-worker: ## Run background queue worker loop
|
||||
cd $(BACKEND_DIR) && uv run python ../scripts/rq worker
|
||||
|
||||
.PHONY: backend-templates-sync
|
||||
backend-templates-sync: ## Sync templates to existing gateway agents (usage: make backend-templates-sync GATEWAY_ID=<uuid> SYNC_ARGS="--reset-sessions")
|
||||
backend-templates-sync: ## Sync templates to existing gateway agents (usage: make backend-templates-sync GATEWAY_ID=<uuid> SYNC_ARGS="--reset-sessions --overwrite")
|
||||
@if [ -z "$(GATEWAY_ID)" ]; then echo "GATEWAY_ID is required (uuid)"; exit 1; fi
|
||||
cd $(BACKEND_DIR) && uv run python scripts/sync_gateway_templates.py --gateway-id "$(GATEWAY_ID)" $(SYNC_ARGS)
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ INCLUDE_MAIN_QUERY = Query(default=True)
|
||||
RESET_SESSIONS_QUERY = Query(default=False)
|
||||
ROTATE_TOKENS_QUERY = Query(default=False)
|
||||
FORCE_BOOTSTRAP_QUERY = Query(default=False)
|
||||
OVERWRITE_QUERY = Query(default=False)
|
||||
LEAD_ONLY_QUERY = Query(default=False)
|
||||
BOARD_ID_QUERY = Query(default=None)
|
||||
_RUNTIME_TYPE_REFERENCES = (UUID,)
|
||||
@@ -53,6 +54,7 @@ def _template_sync_query(
|
||||
reset_sessions: bool = RESET_SESSIONS_QUERY,
|
||||
rotate_tokens: bool = ROTATE_TOKENS_QUERY,
|
||||
force_bootstrap: bool = FORCE_BOOTSTRAP_QUERY,
|
||||
overwrite: bool = OVERWRITE_QUERY,
|
||||
board_id: UUID | None = BOARD_ID_QUERY,
|
||||
) -> GatewayTemplateSyncQuery:
|
||||
return GatewayTemplateSyncQuery(
|
||||
@@ -61,6 +63,7 @@ def _template_sync_query(
|
||||
reset_sessions=reset_sessions,
|
||||
rotate_tokens=rotate_tokens,
|
||||
force_bootstrap=force_bootstrap,
|
||||
overwrite=overwrite,
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ class GatewayAdminLifecycleService(OpenClawDBService):
|
||||
reset_sessions=query.reset_sessions,
|
||||
rotate_tokens=query.rotate_tokens,
|
||||
force_bootstrap=query.force_bootstrap,
|
||||
overwrite=query.overwrite,
|
||||
board_id=query.board_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -61,6 +61,7 @@ class ProvisionOptions:
|
||||
|
||||
action: str = "provision"
|
||||
force_bootstrap: bool = False
|
||||
overwrite: bool = False
|
||||
|
||||
|
||||
_ROLE_SOUL_MAX_CHARS = 24_000
|
||||
@@ -715,6 +716,7 @@ class BaseAgentLifecycleManager(ABC):
|
||||
desired_file_names: set[str] | None = None,
|
||||
existing_files: dict[str, dict[str, Any]],
|
||||
action: str,
|
||||
overwrite: bool = False,
|
||||
) -> None:
|
||||
preserve_files = (
|
||||
self._preserve_files(agent) if agent is not None else set(PRESERVE_AGENT_EDITABLE_FILES)
|
||||
@@ -728,15 +730,10 @@ class BaseAgentLifecycleManager(ABC):
|
||||
# Preserve "editable" files only during updates. During first-time provisioning,
|
||||
# the gateway may pre-create defaults for USER/MEMORY/etc, and we still want to
|
||||
# apply Mission Control's templates.
|
||||
if action == "update" and name in preserve_files:
|
||||
if action == "update" and not overwrite and name in preserve_files:
|
||||
entry = existing_files.get(name)
|
||||
if entry and not bool(entry.get("missing")):
|
||||
size = entry.get("size")
|
||||
if isinstance(size, int) and size == 0:
|
||||
# Treat 0-byte placeholders as missing so update can fill them.
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
continue
|
||||
try:
|
||||
await self._control_plane.set_agent_file(
|
||||
agent_id=agent_id,
|
||||
@@ -840,6 +837,7 @@ class BaseAgentLifecycleManager(ABC):
|
||||
desired_file_names=set(rendered.keys()),
|
||||
existing_files=existing_files,
|
||||
action=options.action,
|
||||
overwrite=options.overwrite,
|
||||
)
|
||||
|
||||
|
||||
@@ -1013,6 +1011,7 @@ class OpenClawGatewayProvisioner:
|
||||
user: User | None,
|
||||
action: str = "provision",
|
||||
force_bootstrap: bool = False,
|
||||
overwrite: bool = False,
|
||||
reset_session: bool = False,
|
||||
wake: bool = True,
|
||||
deliver_wakeup: bool = True,
|
||||
@@ -1056,7 +1055,11 @@ class OpenClawGatewayProvisioner:
|
||||
session_key=session_key,
|
||||
auth_token=auth_token,
|
||||
user=user,
|
||||
options=ProvisionOptions(action=action, force_bootstrap=force_bootstrap),
|
||||
options=ProvisionOptions(
|
||||
action=action,
|
||||
force_bootstrap=force_bootstrap,
|
||||
overwrite=overwrite,
|
||||
),
|
||||
session_label=agent.name or "Gateway Agent",
|
||||
)
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ class GatewayTemplateSyncOptions:
|
||||
reset_sessions: bool = False
|
||||
rotate_tokens: bool = False
|
||||
force_bootstrap: bool = False
|
||||
overwrite: bool = False
|
||||
board_id: UUID | None = None
|
||||
|
||||
|
||||
@@ -569,6 +570,7 @@ async def _sync_one_agent(
|
||||
user=ctx.options.user,
|
||||
action="update",
|
||||
force_bootstrap=ctx.options.force_bootstrap,
|
||||
overwrite=ctx.options.overwrite,
|
||||
reset_session=ctx.options.reset_sessions,
|
||||
wake=False,
|
||||
)
|
||||
@@ -639,6 +641,7 @@ async def _sync_main_agent(
|
||||
user=ctx.options.user,
|
||||
action="update",
|
||||
force_bootstrap=ctx.options.force_bootstrap,
|
||||
overwrite=ctx.options.overwrite,
|
||||
reset_session=ctx.options.reset_sessions,
|
||||
wake=False,
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ class GatewayTemplateSyncQuery:
|
||||
reset_sessions: bool
|
||||
rotate_tokens: bool
|
||||
force_bootstrap: bool
|
||||
overwrite: bool
|
||||
board_id: UUID | None
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@ def _parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Force BOOTSTRAP.md to be rendered during update sync",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite editable files (e.g. USER.md, MEMORY.md) during update sync",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -81,6 +86,7 @@ async def _run() -> int:
|
||||
reset_sessions=bool(args.reset_sessions),
|
||||
rotate_tokens=bool(args.rotate_tokens),
|
||||
force_bootstrap=bool(args.force_bootstrap),
|
||||
overwrite=bool(args.overwrite),
|
||||
board_id=board_id,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -216,8 +216,8 @@ async def test_provision_overwrites_user_md_on_first_provision(monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_agent_files_update_writes_zero_size_user_md():
|
||||
"""Treat empty placeholder files as missing during update."""
|
||||
async def test_set_agent_files_update_preserves_user_md_even_when_size_zero():
|
||||
"""Update should preserve editable files unless overwrite is explicitly requested."""
|
||||
|
||||
class _ControlPlaneStub:
|
||||
def __init__(self):
|
||||
@@ -278,6 +278,135 @@ async def test_set_agent_files_update_writes_zero_size_user_md():
|
||||
existing_files={"USER.md": {"name": "USER.md", "missing": False, "size": 0}},
|
||||
action="update",
|
||||
)
|
||||
assert cp.writes == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_agent_files_update_preserves_nonmissing_user_md():
|
||||
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_agent_files(self, agent_id):
|
||||
return {}
|
||||
|
||||
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]
|
||||
|
||||
await mgr._set_agent_files(
|
||||
agent_id="agent-x",
|
||||
rendered={"USER.md": "filled"},
|
||||
existing_files={"USER.md": {"name": "USER.md", "missing": False}},
|
||||
action="update",
|
||||
)
|
||||
assert cp.writes == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_agent_files_update_overwrite_writes_preserved_user_md():
|
||||
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_agent_files(self, agent_id):
|
||||
return {}
|
||||
|
||||
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]
|
||||
|
||||
await mgr._set_agent_files(
|
||||
agent_id="agent-x",
|
||||
rendered={"USER.md": "filled"},
|
||||
existing_files={"USER.md": {"name": "USER.md", "missing": False}},
|
||||
action="update",
|
||||
overwrite=True,
|
||||
)
|
||||
assert ("USER.md", "filled") in cp.writes
|
||||
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ export type SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams =
|
||||
reset_sessions?: boolean;
|
||||
rotate_tokens?: boolean;
|
||||
force_bootstrap?: boolean;
|
||||
overwrite?: boolean;
|
||||
board_id?: string | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user