@dhf-openclaw/grix 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -143
- package/dist/index.js +1279 -1
- package/package.json +4 -6
- package/skills/egg-install/SKILL.md +170 -0
- package/skills/egg-install/references/api-contract.md +38 -0
- package/skills/grix-agent-admin/SKILL.md +85 -0
- package/skills/grix-agent-admin/agents/openai.yaml +4 -0
- package/skills/grix-agent-admin/references/api-contract.md +87 -0
- package/skills/grix-agent-admin/scripts/grix_agent_bind.py +587 -0
- package/skills/grix-group-governance/SKILL.md +144 -0
- package/skills/grix-group-governance/agents/openai.yaml +4 -0
- package/skills/grix-group-governance/references/api-contract.md +73 -0
- package/skills/grix-query/SKILL.md +151 -0
- package/skills/grix-query/agents/openai.yaml +4 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_OPENCLAW_CONFIG_PATH = "~/.openclaw/openclaw.json"
|
|
12
|
+
DEFAULT_OPENCLAW_TOOLS_PROFILE = "coding"
|
|
13
|
+
DEFAULT_OPENCLAW_TOOLS_VISIBILITY = "agent"
|
|
14
|
+
REQUIRED_OPENCLAW_TOOLS = [
|
|
15
|
+
"message",
|
|
16
|
+
"grix_group",
|
|
17
|
+
"grix_agent_admin",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BindError(RuntimeError):
|
|
22
|
+
def __init__(self, message, payload=None):
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.payload = payload
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_json(payload):
|
|
28
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
29
|
+
sys.stdout.write("\n")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def expand_path(path: str) -> str:
|
|
33
|
+
return os.path.abspath(os.path.expanduser((path or "").strip() or DEFAULT_OPENCLAW_CONFIG_PATH))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_config_path(args) -> str:
|
|
37
|
+
raw_path = str(getattr(args, "config_path", "") or "").strip()
|
|
38
|
+
if raw_path and raw_path != DEFAULT_OPENCLAW_CONFIG_PATH:
|
|
39
|
+
return expand_path(raw_path)
|
|
40
|
+
|
|
41
|
+
profile = str(getattr(args, "openclaw_profile", "") or "").strip()
|
|
42
|
+
if profile:
|
|
43
|
+
return expand_path(f"~/.openclaw-{profile}/openclaw.json")
|
|
44
|
+
|
|
45
|
+
return expand_path(DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_json_file(path: str):
|
|
49
|
+
if not os.path.exists(path):
|
|
50
|
+
return {}
|
|
51
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
52
|
+
raw = handle.read().strip()
|
|
53
|
+
if not raw:
|
|
54
|
+
return {}
|
|
55
|
+
return json.loads(raw)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_json_file_with_backup(path: str, payload):
|
|
59
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
60
|
+
backup_path = ""
|
|
61
|
+
if os.path.exists(path):
|
|
62
|
+
backup_path = f"{path}.bak.{uuid.uuid4().hex[:8]}"
|
|
63
|
+
with open(path, "rb") as src, open(backup_path, "wb") as dst:
|
|
64
|
+
dst.write(src.read())
|
|
65
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
66
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
67
|
+
handle.write("\n")
|
|
68
|
+
return backup_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def normalize_string_list(values):
|
|
72
|
+
if not isinstance(values, list):
|
|
73
|
+
return []
|
|
74
|
+
normalized = []
|
|
75
|
+
seen = set()
|
|
76
|
+
for item in values:
|
|
77
|
+
text = str(item or "").strip()
|
|
78
|
+
if not text or text in seen:
|
|
79
|
+
continue
|
|
80
|
+
seen.add(text)
|
|
81
|
+
normalized.append(text)
|
|
82
|
+
return normalized
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def mask_secret(value: str):
|
|
86
|
+
text = str(value or "").strip()
|
|
87
|
+
if not text:
|
|
88
|
+
return ""
|
|
89
|
+
if len(text) <= 8:
|
|
90
|
+
return "*" * len(text)
|
|
91
|
+
return f"{text[:4]}...{text[-4:]}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def redact_channel_account(account):
|
|
95
|
+
payload = dict(account or {})
|
|
96
|
+
api_key = str(payload.get("apiKey", "")).strip()
|
|
97
|
+
if api_key:
|
|
98
|
+
payload["apiKey"] = "<redacted>"
|
|
99
|
+
payload["apiKeyMasked"] = mask_secret(api_key)
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def shell_command(cmd):
|
|
104
|
+
return " ".join(shlex.quote(part) for part in cmd)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_command_capture(cmd):
|
|
108
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
109
|
+
return {
|
|
110
|
+
"command": shell_command(cmd),
|
|
111
|
+
"returncode": proc.returncode,
|
|
112
|
+
"stdout": proc.stdout.strip(),
|
|
113
|
+
"stderr": proc.stderr.strip(),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_openclaw_base_cmd(args):
|
|
118
|
+
base_cmd = [(args.openclaw_bin or "").strip() or "openclaw"]
|
|
119
|
+
profile = str(getattr(args, "openclaw_profile", "") or "").strip()
|
|
120
|
+
if profile:
|
|
121
|
+
base_cmd.extend(["--profile", profile])
|
|
122
|
+
return base_cmd
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_gateway_restart_command(args):
|
|
126
|
+
return build_openclaw_base_cmd(args) + ["gateway", "restart"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ensure_agent_entry(cfg, target_agent):
|
|
130
|
+
next_cfg = dict(cfg or {})
|
|
131
|
+
agents = dict(next_cfg.get("agents") or {})
|
|
132
|
+
current_list = agents.get("list")
|
|
133
|
+
if not isinstance(current_list, list):
|
|
134
|
+
current_list = []
|
|
135
|
+
next_list = list(current_list)
|
|
136
|
+
|
|
137
|
+
changed = False
|
|
138
|
+
found_index = None
|
|
139
|
+
for idx, item in enumerate(next_list):
|
|
140
|
+
if not isinstance(item, dict):
|
|
141
|
+
continue
|
|
142
|
+
if str(item.get("id", "")).strip() == target_agent["id"]:
|
|
143
|
+
found_index = idx
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
if found_index is None:
|
|
147
|
+
next_list.append(dict(target_agent))
|
|
148
|
+
changed = True
|
|
149
|
+
else:
|
|
150
|
+
existing = dict(next_list[found_index] or {})
|
|
151
|
+
merged = dict(existing)
|
|
152
|
+
merged.update(target_agent)
|
|
153
|
+
if merged != existing:
|
|
154
|
+
next_list[found_index] = merged
|
|
155
|
+
changed = True
|
|
156
|
+
|
|
157
|
+
agents["list"] = next_list
|
|
158
|
+
next_cfg["agents"] = agents
|
|
159
|
+
return next_cfg, changed
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ensure_channel_account(cfg, agent_name: str, target_account):
|
|
163
|
+
next_cfg = dict(cfg or {})
|
|
164
|
+
channels = dict(next_cfg.get("channels") or {})
|
|
165
|
+
grix = dict(channels.get("grix") or {})
|
|
166
|
+
accounts = dict(grix.get("accounts") or {})
|
|
167
|
+
|
|
168
|
+
changed = False
|
|
169
|
+
if not bool(grix.get("enabled", False)):
|
|
170
|
+
grix["enabled"] = True
|
|
171
|
+
changed = True
|
|
172
|
+
|
|
173
|
+
existing = dict(accounts.get(agent_name) or {})
|
|
174
|
+
merged = dict(existing)
|
|
175
|
+
merged.update(target_account)
|
|
176
|
+
if merged != existing:
|
|
177
|
+
accounts[agent_name] = merged
|
|
178
|
+
changed = True
|
|
179
|
+
|
|
180
|
+
grix["accounts"] = accounts
|
|
181
|
+
channels["grix"] = grix
|
|
182
|
+
next_cfg["channels"] = channels
|
|
183
|
+
return next_cfg, changed
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def ensure_route_binding(cfg, agent_name: str):
|
|
187
|
+
next_cfg = dict(cfg or {})
|
|
188
|
+
current_bindings = next_cfg.get("bindings")
|
|
189
|
+
if not isinstance(current_bindings, list):
|
|
190
|
+
current_bindings = []
|
|
191
|
+
bindings = list(current_bindings)
|
|
192
|
+
|
|
193
|
+
changed = False
|
|
194
|
+
best_index = None
|
|
195
|
+
for idx, item in enumerate(bindings):
|
|
196
|
+
if not isinstance(item, dict):
|
|
197
|
+
continue
|
|
198
|
+
if str(item.get("type", "")).strip() != "route":
|
|
199
|
+
continue
|
|
200
|
+
if str(item.get("agentId", "")).strip() != agent_name:
|
|
201
|
+
continue
|
|
202
|
+
match = item.get("match") if isinstance(item.get("match"), dict) else {}
|
|
203
|
+
if str(match.get("channel", "")).strip() != "grix":
|
|
204
|
+
continue
|
|
205
|
+
best_index = idx
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
if best_index is None:
|
|
209
|
+
bindings.append(
|
|
210
|
+
{
|
|
211
|
+
"type": "route",
|
|
212
|
+
"agentId": agent_name,
|
|
213
|
+
"match": {
|
|
214
|
+
"channel": "grix",
|
|
215
|
+
"accountId": agent_name,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
changed = True
|
|
220
|
+
else:
|
|
221
|
+
existing = dict(bindings[best_index] or {})
|
|
222
|
+
match = dict(existing.get("match") or {})
|
|
223
|
+
if str(match.get("accountId", "")).strip() != agent_name:
|
|
224
|
+
match["accountId"] = agent_name
|
|
225
|
+
existing["match"] = match
|
|
226
|
+
bindings[best_index] = existing
|
|
227
|
+
changed = True
|
|
228
|
+
|
|
229
|
+
next_cfg["bindings"] = bindings
|
|
230
|
+
return next_cfg, changed
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def ensure_required_tools(cfg):
|
|
234
|
+
next_cfg = dict(cfg or {})
|
|
235
|
+
tools = dict(next_cfg.get("tools") or {})
|
|
236
|
+
sessions = dict(tools.get("sessions") or {})
|
|
237
|
+
changed = False
|
|
238
|
+
|
|
239
|
+
if str(tools.get("profile", "")).strip() != DEFAULT_OPENCLAW_TOOLS_PROFILE:
|
|
240
|
+
tools["profile"] = DEFAULT_OPENCLAW_TOOLS_PROFILE
|
|
241
|
+
changed = True
|
|
242
|
+
|
|
243
|
+
also_allow = normalize_string_list(tools.get("alsoAllow"))
|
|
244
|
+
next_also_allow = list(also_allow)
|
|
245
|
+
for tool_id in REQUIRED_OPENCLAW_TOOLS:
|
|
246
|
+
if tool_id not in next_also_allow:
|
|
247
|
+
next_also_allow.append(tool_id)
|
|
248
|
+
changed = True
|
|
249
|
+
tools["alsoAllow"] = next_also_allow
|
|
250
|
+
|
|
251
|
+
if str(sessions.get("visibility", "")).strip() != DEFAULT_OPENCLAW_TOOLS_VISIBILITY:
|
|
252
|
+
sessions["visibility"] = DEFAULT_OPENCLAW_TOOLS_VISIBILITY
|
|
253
|
+
changed = True
|
|
254
|
+
tools["sessions"] = sessions
|
|
255
|
+
|
|
256
|
+
next_cfg["tools"] = tools
|
|
257
|
+
return next_cfg, changed
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def resolve_default_model(cfg, current_agent):
|
|
261
|
+
if isinstance(current_agent, dict):
|
|
262
|
+
model = str(current_agent.get("model", "")).strip()
|
|
263
|
+
if model:
|
|
264
|
+
return model
|
|
265
|
+
agents = cfg.get("agents") if isinstance(cfg, dict) else {}
|
|
266
|
+
defaults = agents.get("defaults") if isinstance(agents, dict) else {}
|
|
267
|
+
model_cfg = defaults.get("model") if isinstance(defaults, dict) else {}
|
|
268
|
+
model = str(model_cfg.get("primary", "")).strip() if isinstance(model_cfg, dict) else ""
|
|
269
|
+
return model
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def extract_current_state(cfg, agent_name: str):
|
|
273
|
+
agents = cfg.get("agents") if isinstance(cfg, dict) else {}
|
|
274
|
+
agent_list = agents.get("list") if isinstance(agents, dict) else []
|
|
275
|
+
if not isinstance(agent_list, list):
|
|
276
|
+
agent_list = []
|
|
277
|
+
|
|
278
|
+
current_agent = None
|
|
279
|
+
for item in agent_list:
|
|
280
|
+
if not isinstance(item, dict):
|
|
281
|
+
continue
|
|
282
|
+
if str(item.get("id", "")).strip() == agent_name:
|
|
283
|
+
current_agent = item
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
channels = cfg.get("channels") if isinstance(cfg, dict) else {}
|
|
287
|
+
grix = channels.get("grix") if isinstance(channels, dict) else {}
|
|
288
|
+
accounts = grix.get("accounts") if isinstance(grix, dict) else {}
|
|
289
|
+
current_account = accounts.get(agent_name) if isinstance(accounts, dict) else None
|
|
290
|
+
|
|
291
|
+
current_binding = None
|
|
292
|
+
bindings = cfg.get("bindings") if isinstance(cfg, dict) else []
|
|
293
|
+
if not isinstance(bindings, list):
|
|
294
|
+
bindings = []
|
|
295
|
+
for item in bindings:
|
|
296
|
+
if not isinstance(item, dict):
|
|
297
|
+
continue
|
|
298
|
+
if str(item.get("type", "")).strip() != "route":
|
|
299
|
+
continue
|
|
300
|
+
if str(item.get("agentId", "")).strip() != agent_name:
|
|
301
|
+
continue
|
|
302
|
+
match = item.get("match") if isinstance(item.get("match"), dict) else {}
|
|
303
|
+
if str(match.get("channel", "")).strip() != "grix":
|
|
304
|
+
continue
|
|
305
|
+
current_binding = item
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
tools = cfg.get("tools") if isinstance(cfg, dict) else {}
|
|
309
|
+
if not isinstance(tools, dict):
|
|
310
|
+
tools = {}
|
|
311
|
+
sessions = tools.get("sessions") if isinstance(tools.get("sessions"), dict) else {}
|
|
312
|
+
return {
|
|
313
|
+
"agent_entry": current_agent,
|
|
314
|
+
"channel_account": current_account,
|
|
315
|
+
"route_binding": current_binding,
|
|
316
|
+
"tools_config": {
|
|
317
|
+
"profile": str(tools.get("profile", "")).strip(),
|
|
318
|
+
"alsoAllow": normalize_string_list(tools.get("alsoAllow")),
|
|
319
|
+
"sessions": {
|
|
320
|
+
"visibility": str(sessions.get("visibility", "")).strip(),
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def build_workspace_files(workspace_dir: str, agent_name: str):
|
|
327
|
+
files = {
|
|
328
|
+
"AGENTS.md": f"# {agent_name}\n\nGrix bound agent profile for `{agent_name}`.\n",
|
|
329
|
+
"MEMORY.md": f"# Memory\n\n- owner: {agent_name}\n",
|
|
330
|
+
"USER.md": f"# User\n\nCurrent active account: `{agent_name}`.\n",
|
|
331
|
+
}
|
|
332
|
+
created = []
|
|
333
|
+
os.makedirs(workspace_dir, exist_ok=True)
|
|
334
|
+
for filename, content in files.items():
|
|
335
|
+
path = os.path.join(workspace_dir, filename)
|
|
336
|
+
if os.path.exists(path):
|
|
337
|
+
continue
|
|
338
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
339
|
+
handle.write(content)
|
|
340
|
+
created.append(path)
|
|
341
|
+
return created
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def handle_inspect_local(args):
|
|
345
|
+
agent_name = str(args.agent_name or "").strip()
|
|
346
|
+
if not agent_name:
|
|
347
|
+
raise BindError("--agent-name is required")
|
|
348
|
+
|
|
349
|
+
config_path = resolve_config_path(args)
|
|
350
|
+
cfg = load_json_file(config_path)
|
|
351
|
+
current = extract_current_state(cfg, agent_name)
|
|
352
|
+
|
|
353
|
+
agent_entry = current.get("agent_entry") if isinstance(current, dict) else None
|
|
354
|
+
channel_account = current.get("channel_account") if isinstance(current, dict) else None
|
|
355
|
+
route_binding = current.get("route_binding") if isinstance(current, dict) else None
|
|
356
|
+
tools = current.get("tools_config") if isinstance(current, dict) else {}
|
|
357
|
+
also_allow = normalize_string_list((tools or {}).get("alsoAllow"))
|
|
358
|
+
visibility = str(((tools or {}).get("sessions") or {}).get("visibility", "")).strip()
|
|
359
|
+
profile = str((tools or {}).get("profile", "")).strip()
|
|
360
|
+
|
|
361
|
+
has_required_tools = all(item in also_allow for item in REQUIRED_OPENCLAW_TOOLS)
|
|
362
|
+
tools_ready = profile == DEFAULT_OPENCLAW_TOOLS_PROFILE and has_required_tools and visibility == DEFAULT_OPENCLAW_TOOLS_VISIBILITY
|
|
363
|
+
account_ready = isinstance(channel_account, dict) and bool(str(channel_account.get("apiKey", "")).strip()) and bool(str(channel_account.get("wsUrl", "")).strip()) and bool(str(channel_account.get("agentId", "")).strip())
|
|
364
|
+
binding_ready = isinstance(route_binding, dict)
|
|
365
|
+
|
|
366
|
+
print_json(
|
|
367
|
+
{
|
|
368
|
+
"ok": True,
|
|
369
|
+
"action": "inspect-local-openclaw",
|
|
370
|
+
"config_path": config_path,
|
|
371
|
+
"agent_name": agent_name,
|
|
372
|
+
"ready": bool(agent_entry) and account_ready and binding_ready and tools_ready,
|
|
373
|
+
"checks": {
|
|
374
|
+
"agent_entry_exists": bool(agent_entry),
|
|
375
|
+
"channel_account_ready": account_ready,
|
|
376
|
+
"route_binding_exists": binding_ready,
|
|
377
|
+
"tools_ready": tools_ready,
|
|
378
|
+
},
|
|
379
|
+
"current_state": {
|
|
380
|
+
"agent_entry": agent_entry,
|
|
381
|
+
"channel_account": redact_channel_account(channel_account or {}),
|
|
382
|
+
"route_binding": route_binding,
|
|
383
|
+
"tools_config": tools,
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def handle_configure_local(args):
|
|
390
|
+
agent_name = str(args.agent_name or "").strip()
|
|
391
|
+
agent_id = str(args.agent_id or "").strip()
|
|
392
|
+
api_endpoint = str(args.api_endpoint or "").strip()
|
|
393
|
+
api_key = str(args.api_key or "").strip()
|
|
394
|
+
if not agent_name:
|
|
395
|
+
raise BindError("--agent-name is required")
|
|
396
|
+
if not agent_id:
|
|
397
|
+
raise BindError("--agent-id is required")
|
|
398
|
+
if not api_endpoint:
|
|
399
|
+
raise BindError("--api-endpoint is required")
|
|
400
|
+
if not api_key:
|
|
401
|
+
raise BindError("--api-key is required")
|
|
402
|
+
|
|
403
|
+
config_path = resolve_config_path(args)
|
|
404
|
+
cfg = load_json_file(config_path)
|
|
405
|
+
current = extract_current_state(cfg, agent_name)
|
|
406
|
+
current_agent = current.get("agent_entry") if isinstance(current, dict) else None
|
|
407
|
+
|
|
408
|
+
model = str(args.model or "").strip() or resolve_default_model(cfg, current_agent)
|
|
409
|
+
if not model:
|
|
410
|
+
raise BindError(
|
|
411
|
+
"unable to resolve agent model from args or openclaw config; pass --model explicitly"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
workspace = expand_path(str(args.workspace or "").strip() or f"~/.openclaw/workspace-{agent_name}")
|
|
415
|
+
agent_dir = expand_path(str(args.agent_dir or "").strip() or f"~/.openclaw/agents/{agent_name}/agent")
|
|
416
|
+
|
|
417
|
+
target_agent = {
|
|
418
|
+
"id": agent_name,
|
|
419
|
+
"name": agent_name,
|
|
420
|
+
"workspace": workspace,
|
|
421
|
+
"agentDir": agent_dir,
|
|
422
|
+
"model": model,
|
|
423
|
+
}
|
|
424
|
+
target_account = {
|
|
425
|
+
"name": agent_name,
|
|
426
|
+
"enabled": True,
|
|
427
|
+
"apiKey": api_key,
|
|
428
|
+
"wsUrl": api_endpoint,
|
|
429
|
+
"agentId": agent_id,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
next_cfg = dict(cfg or {})
|
|
433
|
+
change_flags = {
|
|
434
|
+
"agent_entry_updated": False,
|
|
435
|
+
"channel_account_updated": False,
|
|
436
|
+
"route_binding_updated": False,
|
|
437
|
+
"tools_updated": False,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
next_cfg, changed = ensure_agent_entry(next_cfg, target_agent)
|
|
441
|
+
change_flags["agent_entry_updated"] = changed
|
|
442
|
+
next_cfg, changed = ensure_channel_account(next_cfg, agent_name, target_account)
|
|
443
|
+
change_flags["channel_account_updated"] = changed
|
|
444
|
+
next_cfg, changed = ensure_route_binding(next_cfg, agent_name)
|
|
445
|
+
change_flags["route_binding_updated"] = changed
|
|
446
|
+
|
|
447
|
+
if not bool(args.skip_tools_update):
|
|
448
|
+
next_cfg, changed = ensure_required_tools(next_cfg)
|
|
449
|
+
change_flags["tools_updated"] = changed
|
|
450
|
+
|
|
451
|
+
needs_update = any(bool(value) for value in change_flags.values())
|
|
452
|
+
|
|
453
|
+
payload = {
|
|
454
|
+
"ok": True,
|
|
455
|
+
"action": "configure-local-openclaw",
|
|
456
|
+
"apply": bool(args.apply),
|
|
457
|
+
"config_path": config_path,
|
|
458
|
+
"agent_name": agent_name,
|
|
459
|
+
"changes": change_flags,
|
|
460
|
+
"needs_update": needs_update,
|
|
461
|
+
"current_state": {
|
|
462
|
+
"agent_entry": (current or {}).get("agent_entry"),
|
|
463
|
+
"channel_account": redact_channel_account((current or {}).get("channel_account") or {}),
|
|
464
|
+
"route_binding": (current or {}).get("route_binding"),
|
|
465
|
+
"tools_config": (current or {}).get("tools_config"),
|
|
466
|
+
},
|
|
467
|
+
"next_state": {
|
|
468
|
+
"agent_entry": target_agent,
|
|
469
|
+
"channel_account": redact_channel_account(target_account),
|
|
470
|
+
"route_binding": {
|
|
471
|
+
"type": "route",
|
|
472
|
+
"agentId": agent_name,
|
|
473
|
+
"match": {
|
|
474
|
+
"channel": "grix",
|
|
475
|
+
"accountId": agent_name,
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
"tools_requirements": {
|
|
479
|
+
"profile": DEFAULT_OPENCLAW_TOOLS_PROFILE,
|
|
480
|
+
"alsoAllow": list(REQUIRED_OPENCLAW_TOOLS),
|
|
481
|
+
"sessions": {
|
|
482
|
+
"visibility": DEFAULT_OPENCLAW_TOOLS_VISIBILITY,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
"planned_apply_commands": [] if bool(args.skip_gateway_restart) else [shell_command(build_gateway_restart_command(args))],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if args.apply:
|
|
490
|
+
backup_path = ""
|
|
491
|
+
if needs_update:
|
|
492
|
+
backup_path = write_json_file_with_backup(config_path, next_cfg)
|
|
493
|
+
created_paths = []
|
|
494
|
+
created_paths.extend(build_workspace_files(workspace, agent_name))
|
|
495
|
+
os.makedirs(agent_dir, exist_ok=True)
|
|
496
|
+
|
|
497
|
+
command_results = []
|
|
498
|
+
if not bool(args.skip_gateway_restart):
|
|
499
|
+
command_results.append(run_command_capture(build_gateway_restart_command(args)))
|
|
500
|
+
if command_results[-1]["returncode"] != 0:
|
|
501
|
+
raise BindError(
|
|
502
|
+
"openclaw gateway restart failed",
|
|
503
|
+
payload={"command_results": command_results},
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
applied_cfg = load_json_file(config_path)
|
|
507
|
+
applied_state = extract_current_state(applied_cfg, agent_name)
|
|
508
|
+
payload["config_write"] = {
|
|
509
|
+
"changed": needs_update,
|
|
510
|
+
"backup_path": backup_path,
|
|
511
|
+
}
|
|
512
|
+
payload["created_workspace_files"] = created_paths
|
|
513
|
+
payload["command_results"] = command_results
|
|
514
|
+
payload["applied_state"] = {
|
|
515
|
+
"agent_entry": (applied_state or {}).get("agent_entry"),
|
|
516
|
+
"channel_account": redact_channel_account((applied_state or {}).get("channel_account") or {}),
|
|
517
|
+
"route_binding": (applied_state or {}).get("route_binding"),
|
|
518
|
+
"tools_config": (applied_state or {}).get("tools_config"),
|
|
519
|
+
}
|
|
520
|
+
print_json(payload)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def build_parser():
|
|
524
|
+
parser = argparse.ArgumentParser(description="Configure local OpenClaw agent + grix channel binding")
|
|
525
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
526
|
+
|
|
527
|
+
def add_common_local_args(target_parser):
|
|
528
|
+
target_parser.add_argument("--openclaw-bin", dest="openclaw_bin", default="openclaw")
|
|
529
|
+
target_parser.add_argument("--openclaw-profile", dest="openclaw_profile", default="")
|
|
530
|
+
target_parser.add_argument("--config-path", default=DEFAULT_OPENCLAW_CONFIG_PATH)
|
|
531
|
+
|
|
532
|
+
inspect_local = subparsers.add_parser(
|
|
533
|
+
"inspect-local-openclaw",
|
|
534
|
+
help="Inspect local OpenClaw agent + grix account binding state",
|
|
535
|
+
)
|
|
536
|
+
add_common_local_args(inspect_local)
|
|
537
|
+
inspect_local.add_argument("--agent-name", required=True)
|
|
538
|
+
inspect_local.set_defaults(handler=handle_inspect_local)
|
|
539
|
+
|
|
540
|
+
configure_local = subparsers.add_parser(
|
|
541
|
+
"configure-local-openclaw",
|
|
542
|
+
help="Preview or apply local OpenClaw agent + grix account binding",
|
|
543
|
+
)
|
|
544
|
+
add_common_local_args(configure_local)
|
|
545
|
+
configure_local.add_argument("--agent-name", required=True)
|
|
546
|
+
configure_local.add_argument("--agent-id", required=True)
|
|
547
|
+
configure_local.add_argument("--api-endpoint", required=True)
|
|
548
|
+
configure_local.add_argument("--api-key", required=True)
|
|
549
|
+
configure_local.add_argument("--model", default="")
|
|
550
|
+
configure_local.add_argument("--workspace", default="")
|
|
551
|
+
configure_local.add_argument("--agent-dir", default="")
|
|
552
|
+
configure_local.add_argument("--skip-tools-update", action="store_true")
|
|
553
|
+
configure_local.add_argument("--skip-gateway-restart", action="store_true")
|
|
554
|
+
configure_local.add_argument("--apply", action="store_true")
|
|
555
|
+
configure_local.set_defaults(handler=handle_configure_local)
|
|
556
|
+
|
|
557
|
+
return parser
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def main():
|
|
561
|
+
parser = build_parser()
|
|
562
|
+
args = parser.parse_args()
|
|
563
|
+
try:
|
|
564
|
+
args.handler(args)
|
|
565
|
+
except BindError as exc:
|
|
566
|
+
print_json(
|
|
567
|
+
{
|
|
568
|
+
"ok": False,
|
|
569
|
+
"action": args.action,
|
|
570
|
+
"error": str(exc),
|
|
571
|
+
"payload": exc.payload,
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
raise SystemExit(1)
|
|
575
|
+
except Exception as exc:
|
|
576
|
+
print_json(
|
|
577
|
+
{
|
|
578
|
+
"ok": False,
|
|
579
|
+
"action": args.action,
|
|
580
|
+
"error": str(exc),
|
|
581
|
+
}
|
|
582
|
+
)
|
|
583
|
+
raise SystemExit(1)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
if __name__ == "__main__":
|
|
587
|
+
main()
|