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
|
cd $(BACKEND_DIR) && uv run python ../scripts/rq worker
|
||||||
|
|
||||||
.PHONY: backend-templates-sync
|
.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
|
@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)
|
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)
|
RESET_SESSIONS_QUERY = Query(default=False)
|
||||||
ROTATE_TOKENS_QUERY = Query(default=False)
|
ROTATE_TOKENS_QUERY = Query(default=False)
|
||||||
FORCE_BOOTSTRAP_QUERY = Query(default=False)
|
FORCE_BOOTSTRAP_QUERY = Query(default=False)
|
||||||
|
OVERWRITE_QUERY = Query(default=False)
|
||||||
LEAD_ONLY_QUERY = Query(default=False)
|
LEAD_ONLY_QUERY = Query(default=False)
|
||||||
BOARD_ID_QUERY = Query(default=None)
|
BOARD_ID_QUERY = Query(default=None)
|
||||||
_RUNTIME_TYPE_REFERENCES = (UUID,)
|
_RUNTIME_TYPE_REFERENCES = (UUID,)
|
||||||
@@ -53,6 +54,7 @@ def _template_sync_query(
|
|||||||
reset_sessions: bool = RESET_SESSIONS_QUERY,
|
reset_sessions: bool = RESET_SESSIONS_QUERY,
|
||||||
rotate_tokens: bool = ROTATE_TOKENS_QUERY,
|
rotate_tokens: bool = ROTATE_TOKENS_QUERY,
|
||||||
force_bootstrap: bool = FORCE_BOOTSTRAP_QUERY,
|
force_bootstrap: bool = FORCE_BOOTSTRAP_QUERY,
|
||||||
|
overwrite: bool = OVERWRITE_QUERY,
|
||||||
board_id: UUID | None = BOARD_ID_QUERY,
|
board_id: UUID | None = BOARD_ID_QUERY,
|
||||||
) -> GatewayTemplateSyncQuery:
|
) -> GatewayTemplateSyncQuery:
|
||||||
return GatewayTemplateSyncQuery(
|
return GatewayTemplateSyncQuery(
|
||||||
@@ -61,6 +63,7 @@ def _template_sync_query(
|
|||||||
reset_sessions=reset_sessions,
|
reset_sessions=reset_sessions,
|
||||||
rotate_tokens=rotate_tokens,
|
rotate_tokens=rotate_tokens,
|
||||||
force_bootstrap=force_bootstrap,
|
force_bootstrap=force_bootstrap,
|
||||||
|
overwrite=overwrite,
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ class GatewayAdminLifecycleService(OpenClawDBService):
|
|||||||
reset_sessions=query.reset_sessions,
|
reset_sessions=query.reset_sessions,
|
||||||
rotate_tokens=query.rotate_tokens,
|
rotate_tokens=query.rotate_tokens,
|
||||||
force_bootstrap=query.force_bootstrap,
|
force_bootstrap=query.force_bootstrap,
|
||||||
|
overwrite=query.overwrite,
|
||||||
board_id=query.board_id,
|
board_id=query.board_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ProvisionOptions:
|
|||||||
|
|
||||||
action: str = "provision"
|
action: str = "provision"
|
||||||
force_bootstrap: bool = False
|
force_bootstrap: bool = False
|
||||||
|
overwrite: bool = False
|
||||||
|
|
||||||
|
|
||||||
_ROLE_SOUL_MAX_CHARS = 24_000
|
_ROLE_SOUL_MAX_CHARS = 24_000
|
||||||
@@ -715,6 +716,7 @@ class BaseAgentLifecycleManager(ABC):
|
|||||||
desired_file_names: set[str] | None = None,
|
desired_file_names: set[str] | None = None,
|
||||||
existing_files: dict[str, dict[str, Any]],
|
existing_files: dict[str, dict[str, Any]],
|
||||||
action: str,
|
action: str,
|
||||||
|
overwrite: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
preserve_files = (
|
preserve_files = (
|
||||||
self._preserve_files(agent) if agent is not None else set(PRESERVE_AGENT_EDITABLE_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,
|
# 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
|
# the gateway may pre-create defaults for USER/MEMORY/etc, and we still want to
|
||||||
# apply Mission Control's templates.
|
# 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)
|
entry = existing_files.get(name)
|
||||||
if entry and not bool(entry.get("missing")):
|
if entry and not bool(entry.get("missing")):
|
||||||
size = entry.get("size")
|
continue
|
||||||
if isinstance(size, int) and size == 0:
|
|
||||||
# Treat 0-byte placeholders as missing so update can fill them.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
await self._control_plane.set_agent_file(
|
await self._control_plane.set_agent_file(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
@@ -840,6 +837,7 @@ class BaseAgentLifecycleManager(ABC):
|
|||||||
desired_file_names=set(rendered.keys()),
|
desired_file_names=set(rendered.keys()),
|
||||||
existing_files=existing_files,
|
existing_files=existing_files,
|
||||||
action=options.action,
|
action=options.action,
|
||||||
|
overwrite=options.overwrite,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1013,6 +1011,7 @@ class OpenClawGatewayProvisioner:
|
|||||||
user: User | None,
|
user: User | None,
|
||||||
action: str = "provision",
|
action: str = "provision",
|
||||||
force_bootstrap: bool = False,
|
force_bootstrap: bool = False,
|
||||||
|
overwrite: bool = False,
|
||||||
reset_session: bool = False,
|
reset_session: bool = False,
|
||||||
wake: bool = True,
|
wake: bool = True,
|
||||||
deliver_wakeup: bool = True,
|
deliver_wakeup: bool = True,
|
||||||
@@ -1056,7 +1055,11 @@ class OpenClawGatewayProvisioner:
|
|||||||
session_key=session_key,
|
session_key=session_key,
|
||||||
auth_token=auth_token,
|
auth_token=auth_token,
|
||||||
user=user,
|
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",
|
session_label=agent.name or "Gateway Agent",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class GatewayTemplateSyncOptions:
|
|||||||
reset_sessions: bool = False
|
reset_sessions: bool = False
|
||||||
rotate_tokens: bool = False
|
rotate_tokens: bool = False
|
||||||
force_bootstrap: bool = False
|
force_bootstrap: bool = False
|
||||||
|
overwrite: bool = False
|
||||||
board_id: UUID | None = None
|
board_id: UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -569,6 +570,7 @@ async def _sync_one_agent(
|
|||||||
user=ctx.options.user,
|
user=ctx.options.user,
|
||||||
action="update",
|
action="update",
|
||||||
force_bootstrap=ctx.options.force_bootstrap,
|
force_bootstrap=ctx.options.force_bootstrap,
|
||||||
|
overwrite=ctx.options.overwrite,
|
||||||
reset_session=ctx.options.reset_sessions,
|
reset_session=ctx.options.reset_sessions,
|
||||||
wake=False,
|
wake=False,
|
||||||
)
|
)
|
||||||
@@ -639,6 +641,7 @@ async def _sync_main_agent(
|
|||||||
user=ctx.options.user,
|
user=ctx.options.user,
|
||||||
action="update",
|
action="update",
|
||||||
force_bootstrap=ctx.options.force_bootstrap,
|
force_bootstrap=ctx.options.force_bootstrap,
|
||||||
|
overwrite=ctx.options.overwrite,
|
||||||
reset_session=ctx.options.reset_sessions,
|
reset_session=ctx.options.reset_sessions,
|
||||||
wake=False,
|
wake=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class GatewayTemplateSyncQuery:
|
|||||||
reset_sessions: bool
|
reset_sessions: bool
|
||||||
rotate_tokens: bool
|
rotate_tokens: bool
|
||||||
force_bootstrap: bool
|
force_bootstrap: bool
|
||||||
|
overwrite: bool
|
||||||
board_id: UUID | None
|
board_id: UUID | None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Force BOOTSTRAP.md to be rendered during update sync",
|
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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +86,7 @@ async def _run() -> int:
|
|||||||
reset_sessions=bool(args.reset_sessions),
|
reset_sessions=bool(args.reset_sessions),
|
||||||
rotate_tokens=bool(args.rotate_tokens),
|
rotate_tokens=bool(args.rotate_tokens),
|
||||||
force_bootstrap=bool(args.force_bootstrap),
|
force_bootstrap=bool(args.force_bootstrap),
|
||||||
|
overwrite=bool(args.overwrite),
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ async def test_provision_overwrites_user_md_on_first_provision(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_set_agent_files_update_writes_zero_size_user_md():
|
async def test_set_agent_files_update_preserves_user_md_even_when_size_zero():
|
||||||
"""Treat empty placeholder files as missing during update."""
|
"""Update should preserve editable files unless overwrite is explicitly requested."""
|
||||||
|
|
||||||
class _ControlPlaneStub:
|
class _ControlPlaneStub:
|
||||||
def __init__(self):
|
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}},
|
existing_files={"USER.md": {"name": "USER.md", "missing": False, "size": 0}},
|
||||||
action="update",
|
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
|
assert ("USER.md", "filled") in cp.writes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ export type SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams =
|
|||||||
reset_sessions?: boolean;
|
reset_sessions?: boolean;
|
||||||
rotate_tokens?: boolean;
|
rotate_tokens?: boolean;
|
||||||
force_bootstrap?: boolean;
|
force_bootstrap?: boolean;
|
||||||
|
overwrite?: boolean;
|
||||||
board_id?: string | null;
|
board_id?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user