@dhf-openclaw/grix 0.4.10 → 0.4.13
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 +182 -166
- package/dist/index.js +347 -87
- package/package.json +2 -2
- package/skills/grix-admin/SKILL.md +81 -0
- package/skills/{grix-agent-admin → grix-admin}/references/api-contract.md +22 -3
- package/skills/{egg-install → grix-egg}/SKILL.md +1 -1
- package/skills/{grix-group-governance → grix-group}/SKILL.md +21 -5
- package/skills/{grix-group-governance → grix-group}/references/api-contract.md +15 -0
- package/skills/grix-register/SKILL.md +80 -0
- package/skills/grix-register/references/api-contract.md +72 -0
- package/skills/grix-register/references/grix-concepts.md +26 -0
- package/skills/grix-register/references/handoff-contract.md +24 -0
- package/skills/grix-register/references/openclaw-setup.md +6 -0
- package/skills/grix-register/references/user-replies.md +25 -0
- package/skills/grix-register/scripts/grix_auth.py +474 -0
- package/skills/grix-agent-admin/SKILL.md +0 -85
- package/skills/grix-agent-admin/agents/openai.yaml +0 -4
- package/skills/grix-group-governance/agents/openai.yaml +0 -4
- package/skills/grix-query/agents/openai.yaml +0 -4
- /package/skills/{grix-agent-admin → grix-admin}/scripts/grix_agent_bind.py +0 -0
- /package/skills/{egg-install → grix-egg}/references/api-contract.md +0 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://grix.dhf.pub"
|
|
15
|
+
DEFAULT_TIMEOUT_SECONDS = 15
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_default_base_url() -> str:
|
|
19
|
+
return (os.environ.get("GRIX_WEB_BASE_URL", "") or "").strip() or DEFAULT_BASE_URL
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def derive_portal_url(raw_base_url: str) -> str:
|
|
23
|
+
base = (raw_base_url or "").strip() or resolve_default_base_url()
|
|
24
|
+
parsed = urllib.parse.urlparse(base)
|
|
25
|
+
if not parsed.scheme or not parsed.netloc:
|
|
26
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
27
|
+
|
|
28
|
+
path = parsed.path.rstrip("/")
|
|
29
|
+
if path.endswith("/v1"):
|
|
30
|
+
path = path[: -len("/v1")]
|
|
31
|
+
|
|
32
|
+
normalized = parsed._replace(path=path or "/", params="", query="", fragment="")
|
|
33
|
+
return urllib.parse.urlunparse(normalized).rstrip("/") + "/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GrixAuthError(RuntimeError):
|
|
37
|
+
def __init__(self, message, status=0, code=-1, payload=None):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
self.code = code
|
|
41
|
+
self.payload = payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_base_url(raw_base_url: str) -> str:
|
|
45
|
+
base = (raw_base_url or "").strip() or resolve_default_base_url()
|
|
46
|
+
parsed = urllib.parse.urlparse(base)
|
|
47
|
+
if not parsed.scheme or not parsed.netloc:
|
|
48
|
+
raise ValueError(f"Invalid base URL: {base}")
|
|
49
|
+
|
|
50
|
+
path = parsed.path.rstrip("/")
|
|
51
|
+
if not path:
|
|
52
|
+
path = "/v1"
|
|
53
|
+
elif not path.endswith("/v1"):
|
|
54
|
+
path = f"{path}/v1"
|
|
55
|
+
|
|
56
|
+
normalized = parsed._replace(path=path, params="", query="", fragment="")
|
|
57
|
+
return urllib.parse.urlunparse(normalized).rstrip("/")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def request_json(method: str, path: str, base_url: str, body=None, headers=None):
|
|
61
|
+
api_base_url = normalize_base_url(base_url)
|
|
62
|
+
url = f"{api_base_url}{path if path.startswith('/') else '/' + path}"
|
|
63
|
+
data = None
|
|
64
|
+
final_headers = dict(headers or {})
|
|
65
|
+
if body is not None:
|
|
66
|
+
data = json.dumps(body).encode("utf-8")
|
|
67
|
+
final_headers["Content-Type"] = "application/json"
|
|
68
|
+
|
|
69
|
+
req = urllib.request.Request(url=url, data=data, headers=final_headers, method=method)
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT_SECONDS) as resp:
|
|
72
|
+
status = getattr(resp, "status", 200)
|
|
73
|
+
raw = resp.read().decode("utf-8")
|
|
74
|
+
except urllib.error.HTTPError as exc:
|
|
75
|
+
status = exc.code
|
|
76
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
77
|
+
except urllib.error.URLError as exc:
|
|
78
|
+
raise GrixAuthError(f"network error: {exc.reason}") from exc
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
payload = json.loads(raw)
|
|
82
|
+
except json.JSONDecodeError as exc:
|
|
83
|
+
raise GrixAuthError(f"invalid json response: {raw[:256]}", status=status) from exc
|
|
84
|
+
|
|
85
|
+
code = int(payload.get("code", -1))
|
|
86
|
+
msg = str(payload.get("msg", "")).strip() or "unknown error"
|
|
87
|
+
if status >= 400 or code != 0:
|
|
88
|
+
raise GrixAuthError(msg, status=status, code=code, payload=payload)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"api_base_url": api_base_url,
|
|
92
|
+
"status": status,
|
|
93
|
+
"data": payload.get("data"),
|
|
94
|
+
"payload": payload,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def maybe_write_captcha_image(b64s: str):
|
|
99
|
+
text = (b64s or "").strip()
|
|
100
|
+
if not text.startswith("data:image/"):
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
marker = ";base64,"
|
|
104
|
+
idx = text.find(marker)
|
|
105
|
+
if idx < 0:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
encoded = text[idx + len(marker) :]
|
|
109
|
+
try:
|
|
110
|
+
content = base64.b64decode(encoded)
|
|
111
|
+
except Exception:
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
fd, path = tempfile.mkstemp(prefix="grix-captcha-", suffix=".png")
|
|
115
|
+
try:
|
|
116
|
+
with os.fdopen(fd, "wb") as handle:
|
|
117
|
+
handle.write(content)
|
|
118
|
+
except Exception:
|
|
119
|
+
try:
|
|
120
|
+
os.unlink(path)
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
return ""
|
|
124
|
+
return path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def print_json(payload):
|
|
128
|
+
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
|
|
129
|
+
sys.stdout.write("\n")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_auth_result(action: str, result: dict, base_url: str):
|
|
133
|
+
data = result.get("data") or {}
|
|
134
|
+
user = data.get("user") or {}
|
|
135
|
+
return {
|
|
136
|
+
"ok": True,
|
|
137
|
+
"action": action,
|
|
138
|
+
"api_base_url": result["api_base_url"],
|
|
139
|
+
"access_token": data.get("access_token", ""),
|
|
140
|
+
"refresh_token": data.get("refresh_token", ""),
|
|
141
|
+
"expires_in": data.get("expires_in", 0),
|
|
142
|
+
"user_id": user.get("id", ""),
|
|
143
|
+
"portal_url": derive_portal_url(base_url),
|
|
144
|
+
"data": data,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_agent_result(action: str, result: dict):
|
|
149
|
+
data = result.get("data") or {}
|
|
150
|
+
agent_id = str(data.get("id", "")).strip()
|
|
151
|
+
api_endpoint = str(data.get("api_endpoint", "")).strip()
|
|
152
|
+
api_key = str(data.get("api_key", "")).strip()
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"ok": True,
|
|
156
|
+
"action": action,
|
|
157
|
+
"api_base_url": result["api_base_url"],
|
|
158
|
+
"agent_id": agent_id,
|
|
159
|
+
"agent_name": data.get("agent_name", ""),
|
|
160
|
+
"provider_type": data.get("provider_type", 0),
|
|
161
|
+
"api_endpoint": api_endpoint,
|
|
162
|
+
"api_key": api_key,
|
|
163
|
+
"api_key_hint": data.get("api_key_hint", ""),
|
|
164
|
+
"session_id": data.get("session_id", ""),
|
|
165
|
+
"handoff": {
|
|
166
|
+
"target_skill": "grix-admin",
|
|
167
|
+
"payload": {
|
|
168
|
+
"agent_id": agent_id,
|
|
169
|
+
"agent_name": data.get("agent_name", ""),
|
|
170
|
+
"api_endpoint": api_endpoint,
|
|
171
|
+
"api_key": api_key,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
"data": data,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def login_with_credentials(base_url: str, account: str, password: str, device_id: str, platform: str):
|
|
179
|
+
result = request_json(
|
|
180
|
+
"POST",
|
|
181
|
+
"/auth/login",
|
|
182
|
+
base_url,
|
|
183
|
+
body={
|
|
184
|
+
"account": account,
|
|
185
|
+
"password": password,
|
|
186
|
+
"device_id": device_id,
|
|
187
|
+
"platform": platform,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
return build_auth_result("login", result, base_url)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def create_api_agent(base_url: str, access_token: str, agent_name: str, avatar_url: str):
|
|
194
|
+
request_body = {
|
|
195
|
+
"agent_name": agent_name.strip(),
|
|
196
|
+
"provider_type": 3,
|
|
197
|
+
"is_main": True,
|
|
198
|
+
}
|
|
199
|
+
normalized_avatar_url = (avatar_url or "").strip()
|
|
200
|
+
if normalized_avatar_url:
|
|
201
|
+
request_body["avatar_url"] = normalized_avatar_url
|
|
202
|
+
|
|
203
|
+
result = request_json(
|
|
204
|
+
"POST",
|
|
205
|
+
"/agents/create",
|
|
206
|
+
base_url,
|
|
207
|
+
body=request_body,
|
|
208
|
+
headers={
|
|
209
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
return build_agent_result("create-api-agent", result)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def list_agents(base_url: str, access_token: str):
|
|
216
|
+
result = request_json(
|
|
217
|
+
"GET",
|
|
218
|
+
"/agents/list",
|
|
219
|
+
base_url,
|
|
220
|
+
headers={
|
|
221
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
data = result.get("data") or {}
|
|
225
|
+
items = data.get("list") or []
|
|
226
|
+
if not isinstance(items, list):
|
|
227
|
+
items = []
|
|
228
|
+
return items
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def rotate_api_agent_key(base_url: str, access_token: str, agent_id: str):
|
|
232
|
+
result = request_json(
|
|
233
|
+
"POST",
|
|
234
|
+
f"/agents/{str(agent_id).strip()}/api/key/rotate",
|
|
235
|
+
base_url,
|
|
236
|
+
body={},
|
|
237
|
+
headers={
|
|
238
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
return build_agent_result("rotate-api-agent-key", result)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def find_existing_api_agent(agents, agent_name: str):
|
|
245
|
+
normalized_name = (agent_name or "").strip()
|
|
246
|
+
if not normalized_name:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
for item in agents:
|
|
250
|
+
if not isinstance(item, dict):
|
|
251
|
+
continue
|
|
252
|
+
if str(item.get("agent_name", "")).strip() != normalized_name:
|
|
253
|
+
continue
|
|
254
|
+
if int(item.get("provider_type", 0) or 0) != 3:
|
|
255
|
+
continue
|
|
256
|
+
if int(item.get("status", 0) or 0) == 3:
|
|
257
|
+
continue
|
|
258
|
+
return item
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def create_or_reuse_api_agent(
|
|
263
|
+
base_url: str,
|
|
264
|
+
access_token: str,
|
|
265
|
+
agent_name: str,
|
|
266
|
+
avatar_url: str,
|
|
267
|
+
prefer_existing: bool,
|
|
268
|
+
rotate_on_reuse: bool,
|
|
269
|
+
):
|
|
270
|
+
if prefer_existing:
|
|
271
|
+
agents = list_agents(base_url, access_token)
|
|
272
|
+
existing = find_existing_api_agent(agents, agent_name)
|
|
273
|
+
if existing is not None:
|
|
274
|
+
if not rotate_on_reuse:
|
|
275
|
+
raise GrixAuthError(
|
|
276
|
+
"existing provider_type=3 agent found but rotate-on-reuse is disabled; cannot obtain api_key safely",
|
|
277
|
+
payload={"existing_agent": existing},
|
|
278
|
+
)
|
|
279
|
+
rotated = rotate_api_agent_key(base_url, access_token, str(existing.get("id", "")).strip())
|
|
280
|
+
rotated["source"] = "reused_existing_agent_with_rotated_key"
|
|
281
|
+
rotated["existing_agent"] = existing
|
|
282
|
+
return rotated
|
|
283
|
+
|
|
284
|
+
created = create_api_agent(base_url, access_token, agent_name, avatar_url)
|
|
285
|
+
created["source"] = "created_new_agent"
|
|
286
|
+
return created
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def default_device_id(platform: str) -> str:
|
|
290
|
+
normalized_platform = (platform or "").strip() or "web"
|
|
291
|
+
return f"{normalized_platform}_{uuid.uuid4()}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def handle_fetch_captcha(args):
|
|
295
|
+
result = request_json("GET", "/auth/captcha", args.base_url)
|
|
296
|
+
data = result.get("data") or {}
|
|
297
|
+
image_path = maybe_write_captcha_image(str(data.get("b64s", "")))
|
|
298
|
+
payload = {
|
|
299
|
+
"ok": True,
|
|
300
|
+
"action": "fetch-captcha",
|
|
301
|
+
"api_base_url": result["api_base_url"],
|
|
302
|
+
"captcha_id": data.get("captcha_id", ""),
|
|
303
|
+
"b64s": data.get("b64s", ""),
|
|
304
|
+
}
|
|
305
|
+
if image_path:
|
|
306
|
+
payload["captcha_image_path"] = image_path
|
|
307
|
+
print_json(payload)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def handle_send_email_code(args):
|
|
311
|
+
scene = args.scene.strip()
|
|
312
|
+
payload = {
|
|
313
|
+
"email": args.email.strip(),
|
|
314
|
+
"scene": scene,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
captcha_id = (args.captcha_id or "").strip()
|
|
318
|
+
captcha_value = (args.captcha_value or "").strip()
|
|
319
|
+
if scene in {"reset", "change_password"}:
|
|
320
|
+
if not captcha_id or not captcha_value:
|
|
321
|
+
raise GrixAuthError("captcha_id and captcha_value are required for reset/change_password")
|
|
322
|
+
if captcha_id:
|
|
323
|
+
payload["captcha_id"] = captcha_id
|
|
324
|
+
if captcha_value:
|
|
325
|
+
payload["captcha_value"] = captcha_value
|
|
326
|
+
|
|
327
|
+
result = request_json(
|
|
328
|
+
"POST",
|
|
329
|
+
"/auth/send-code",
|
|
330
|
+
args.base_url,
|
|
331
|
+
body=payload,
|
|
332
|
+
)
|
|
333
|
+
print_json(
|
|
334
|
+
{
|
|
335
|
+
"ok": True,
|
|
336
|
+
"action": "send-email-code",
|
|
337
|
+
"api_base_url": result["api_base_url"],
|
|
338
|
+
"data": result.get("data"),
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def handle_register(args):
|
|
344
|
+
platform = (args.platform or "").strip() or "web"
|
|
345
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
346
|
+
result = request_json(
|
|
347
|
+
"POST",
|
|
348
|
+
"/auth/register",
|
|
349
|
+
args.base_url,
|
|
350
|
+
body={
|
|
351
|
+
"email": args.email.strip(),
|
|
352
|
+
"password": args.password.strip(),
|
|
353
|
+
"email_code": args.email_code.strip(),
|
|
354
|
+
"device_id": device_id,
|
|
355
|
+
"platform": platform,
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
print_json(build_auth_result("register", result, args.base_url))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def handle_login(args):
|
|
362
|
+
account = (args.email or args.account or "").strip()
|
|
363
|
+
if not account:
|
|
364
|
+
raise GrixAuthError("either --email or --account is required")
|
|
365
|
+
|
|
366
|
+
platform = (args.platform or "").strip() or "web"
|
|
367
|
+
device_id = (args.device_id or "").strip() or default_device_id(platform)
|
|
368
|
+
print_json(
|
|
369
|
+
login_with_credentials(
|
|
370
|
+
args.base_url,
|
|
371
|
+
account,
|
|
372
|
+
args.password.strip(),
|
|
373
|
+
device_id,
|
|
374
|
+
platform,
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def handle_create_api_agent(args):
|
|
380
|
+
print_json(
|
|
381
|
+
create_or_reuse_api_agent(
|
|
382
|
+
args.base_url,
|
|
383
|
+
args.access_token.strip(),
|
|
384
|
+
args.agent_name.strip(),
|
|
385
|
+
args.avatar_url,
|
|
386
|
+
not bool(args.no_reuse_existing_agent),
|
|
387
|
+
not bool(args.no_rotate_key_on_reuse),
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def build_parser():
|
|
393
|
+
parser = argparse.ArgumentParser(description="Grix public auth API helper")
|
|
394
|
+
parser.add_argument(
|
|
395
|
+
"--base-url",
|
|
396
|
+
default=resolve_default_base_url(),
|
|
397
|
+
help="Grix web base URL (defaults to GRIX_WEB_BASE_URL or https://grix.dhf.pub)",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
401
|
+
|
|
402
|
+
fetch_captcha = subparsers.add_parser("fetch-captcha", help="Fetch captcha image")
|
|
403
|
+
fetch_captcha.set_defaults(handler=handle_fetch_captcha)
|
|
404
|
+
|
|
405
|
+
send_email_code = subparsers.add_parser("send-email-code", help="Send email verification code")
|
|
406
|
+
send_email_code.add_argument("--email", required=True)
|
|
407
|
+
send_email_code.add_argument("--scene", required=True, choices=["register", "reset", "change_password"])
|
|
408
|
+
send_email_code.add_argument("--captcha-id", default="")
|
|
409
|
+
send_email_code.add_argument("--captcha-value", default="")
|
|
410
|
+
send_email_code.set_defaults(handler=handle_send_email_code)
|
|
411
|
+
|
|
412
|
+
register = subparsers.add_parser("register", help="Register by email verification code")
|
|
413
|
+
register.add_argument("--email", required=True)
|
|
414
|
+
register.add_argument("--password", required=True)
|
|
415
|
+
register.add_argument("--email-code", required=True)
|
|
416
|
+
register.add_argument("--device-id", default="")
|
|
417
|
+
register.add_argument("--platform", default="web")
|
|
418
|
+
register.set_defaults(handler=handle_register)
|
|
419
|
+
|
|
420
|
+
login = subparsers.add_parser("login", help="Login and obtain tokens")
|
|
421
|
+
login_identity = login.add_mutually_exclusive_group(required=True)
|
|
422
|
+
login_identity.add_argument("--account")
|
|
423
|
+
login_identity.add_argument("--email")
|
|
424
|
+
login.add_argument("--password", required=True)
|
|
425
|
+
login.add_argument("--device-id", default="")
|
|
426
|
+
login.add_argument("--platform", default="web")
|
|
427
|
+
login.set_defaults(handler=handle_login)
|
|
428
|
+
|
|
429
|
+
create_api_agent_parser = subparsers.add_parser(
|
|
430
|
+
"create-api-agent",
|
|
431
|
+
help="Create a provider_type=3 API agent with a user access token",
|
|
432
|
+
)
|
|
433
|
+
create_api_agent_parser.add_argument("--access-token", required=True)
|
|
434
|
+
create_api_agent_parser.add_argument("--agent-name", required=True)
|
|
435
|
+
create_api_agent_parser.add_argument("--avatar-url", default="")
|
|
436
|
+
create_api_agent_parser.add_argument("--no-reuse-existing-agent", action="store_true")
|
|
437
|
+
create_api_agent_parser.add_argument("--no-rotate-key-on-reuse", action="store_true")
|
|
438
|
+
create_api_agent_parser.set_defaults(handler=handle_create_api_agent)
|
|
439
|
+
|
|
440
|
+
return parser
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def main():
|
|
444
|
+
parser = build_parser()
|
|
445
|
+
args = parser.parse_args()
|
|
446
|
+
try:
|
|
447
|
+
args.handler(args)
|
|
448
|
+
except GrixAuthError as exc:
|
|
449
|
+
print_json(
|
|
450
|
+
{
|
|
451
|
+
"ok": False,
|
|
452
|
+
"action": args.action,
|
|
453
|
+
"status": exc.status,
|
|
454
|
+
"code": exc.code,
|
|
455
|
+
"error": str(exc),
|
|
456
|
+
"payload": exc.payload,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
raise SystemExit(1)
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
print_json(
|
|
462
|
+
{
|
|
463
|
+
"ok": False,
|
|
464
|
+
"action": args.action,
|
|
465
|
+
"status": 0,
|
|
466
|
+
"code": -1,
|
|
467
|
+
"error": str(exc),
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
raise SystemExit(1)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
if __name__ == "__main__":
|
|
474
|
+
main()
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: grix-agent-admin
|
|
3
|
-
description: 通过 Grix Agent API 协议创建 `provider_type=3` 的 API agent,并直接完成本地 OpenClaw agent 与 Grix 渠道绑定配置(默认直接应用并返回结果)。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Grix Agent Admin
|
|
7
|
-
|
|
8
|
-
Create a remote `provider_type=3` API agent, then complete local OpenClaw agent + grix channel binding in one flow.
|
|
9
|
-
|
|
10
|
-
## Security + Auth Path
|
|
11
|
-
|
|
12
|
-
1. This skill does **not** ask the user for website account/password.
|
|
13
|
-
2. Remote create action uses local `channels.grix` credentials and `Authorization: Bearer <agent_api_key>`.
|
|
14
|
-
3. Local OpenClaw config is handled by `scripts/grix_agent_bind.py`.
|
|
15
|
-
|
|
16
|
-
## Required Input
|
|
17
|
-
|
|
18
|
-
1. `agentName` (required): regex `^[a-z][a-z0-9-]{2,31}$`
|
|
19
|
-
2. `describeMessageTool` (required): must contain non-empty `actions`
|
|
20
|
-
3. `accountId` (optional)
|
|
21
|
-
4. `avatarUrl` (optional)
|
|
22
|
-
|
|
23
|
-
## Full Workflow
|
|
24
|
-
|
|
25
|
-
### A. Create Remote API Agent
|
|
26
|
-
|
|
27
|
-
1. Validate `agentName` and `describeMessageTool`.
|
|
28
|
-
2. Call `grix_agent_admin` once.
|
|
29
|
-
3. Read result fields: `id`, `agent_name`, `api_endpoint`, `api_key`, `api_key_hint`.
|
|
30
|
-
|
|
31
|
-
### B. Apply Local OpenClaw Binding Directly
|
|
32
|
-
|
|
33
|
-
Run with `--apply` directly:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
scripts/grix_agent_bind.py configure-local-openclaw \
|
|
37
|
-
--agent-name <agent_name> \
|
|
38
|
-
--agent-id <agent_id> \
|
|
39
|
-
--api-endpoint '<api_endpoint>' \
|
|
40
|
-
--api-key '<api_key>' \
|
|
41
|
-
--apply
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
This applies:
|
|
45
|
-
|
|
46
|
-
1. upsert `agents.list` for `<agent_name>`
|
|
47
|
-
2. upsert `channels.grix.accounts.<agent_name>`
|
|
48
|
-
3. upsert `bindings` route to `channel=grix`, `accountId=<agent_name>`
|
|
49
|
-
4. ensure required tools (`message`, `grix_group`, `grix_agent_admin`)
|
|
50
|
-
5. create workspace defaults under `~/.openclaw/workspace-<agent_name>/`
|
|
51
|
-
6. run `openclaw gateway restart`
|
|
52
|
-
|
|
53
|
-
### C. Optional Verification
|
|
54
|
-
|
|
55
|
-
If you need explicit post-check state, run:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
scripts/grix_agent_bind.py inspect-local-openclaw --agent-name <agent_name>
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Guardrails
|
|
62
|
-
|
|
63
|
-
1. Never ask user for website account/password.
|
|
64
|
-
2. Treat remote create as non-idempotent; do not auto-retry without confirmation.
|
|
65
|
-
3. Keep full `api_key` one-time only; do not repeatedly echo it.
|
|
66
|
-
4. Do not claim success before apply command returns success.
|
|
67
|
-
|
|
68
|
-
## Error Handling Rules
|
|
69
|
-
|
|
70
|
-
1. invalid name: ask user for a valid English lowercase name.
|
|
71
|
-
2. `403/20011`: ask owner to grant `agent.api.create` scope.
|
|
72
|
-
3. `401/10001`: verify local `agent_api_key` / grix account config.
|
|
73
|
-
4. `409/20002`: ask for another agent name.
|
|
74
|
-
5. local apply failed: return concrete failed command/result and stop.
|
|
75
|
-
|
|
76
|
-
## Response Style
|
|
77
|
-
|
|
78
|
-
1. Report two stages separately: remote create status + local binding status.
|
|
79
|
-
2. Include created `agent_id`, `agent_name`, `api_endpoint`, `api_key_hint`.
|
|
80
|
-
3. Clearly state local config has been applied (or failed with concrete reason).
|
|
81
|
-
|
|
82
|
-
## References
|
|
83
|
-
|
|
84
|
-
1. Load [references/api-contract.md](references/api-contract.md).
|
|
85
|
-
2. Use [scripts/grix_agent_bind.py](scripts/grix_agent_bind.py) for local binding apply.
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Grix Agent Admin"
|
|
3
|
-
short_description: "Create Grix API agents and finish local OpenClaw binding."
|
|
4
|
-
default_prompt: "Use this skill when users ask to create a new Grix API agent and finish OpenClaw binding. Never ask for website account/password. Validate `agentName` and require `describeMessageTool` (`actions` must be non-empty), then call `grix_agent_admin` once. After successful create, directly run `scripts/grix_agent_bind.py configure-local-openclaw ... --apply` and return final local binding result (success or exact failure reason)."
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Grix Group Governance"
|
|
3
|
-
short_description: "Use the typed grix_group tool for Grix group operations."
|
|
4
|
-
default_prompt: "Use this skill when users ask to create, inspect, update, or dissolve Grix groups. Validate parameters, call grix_group exactly once per action, and return clear remediation for scope/auth/parameter failures."
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
version: 1
|
|
2
|
-
agent:
|
|
3
|
-
name: grix-query
|
|
4
|
-
default_prompt: "Use this skill when users ask to find Grix contacts, locate sessions, or inspect a known session's message history. Validate required parameters, call grix_query exactly once per action, and do not fabricate a sessionId."
|
|
File without changes
|
|
File without changes
|