refactor: add overwrite option to various services and update documentation

This commit is contained in:
Abhimanyu Saharan
2026-02-15 13:55:47 +05:30
parent 2b96504712
commit 1996e21695
9 changed files with 158 additions and 11 deletions

View File

@@ -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,
)

View File

@@ -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,
),
)

View File

@@ -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",
)

View File

@@ -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,
)

View File

@@ -48,6 +48,7 @@ class GatewayTemplateSyncQuery:
reset_sessions: bool
rotate_tokens: bool
force_bootstrap: bool
overwrite: bool
board_id: UUID | None

View File

@@ -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,
),
)

View File

@@ -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