arkclaw-webchat-cli 0.6.0__tar.gz → 0.6.2__tar.gz

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.
Files changed (25) hide show
  1. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/PKG-INFO +8 -5
  2. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/README.md +7 -4
  3. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/pyproject.toml +1 -1
  4. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/cli.py +91 -9
  5. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/flows.py +280 -73
  6. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/openclaw.py +61 -3
  7. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/.gitignore +0 -0
  8. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/__init__.py +0 -0
  9. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/attachments.py +0 -0
  10. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/config.py +0 -0
  11. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/control.py +0 -0
  12. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/core.py +0 -0
  13. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/doctor.py +0 -0
  14. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/errors.py +0 -0
  15. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/identity.py +0 -0
  16. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/oauth.py +0 -0
  17. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/output.py +0 -0
  18. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/policy.py +0 -0
  19. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/providers.py +0 -0
  20. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/secrets_store.py +0 -0
  21. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/sts.py +0 -0
  22. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/__init__.py +0 -0
  23. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/a2a.py +0 -0
  24. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/transport/base.py +0 -0
  25. {arkclaw_webchat_cli-0.6.0 → arkclaw_webchat_cli-0.6.2}/src/ee_claw/update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkclaw-webchat-cli
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: CLI to chat with an ArkClaw EE space's Claw over enterprise SSO — zero permanent AK/SK.
5
5
  Author: ArkClaw Team
6
6
  Keywords: arkclaw,cli,ee,openclaw,sso,sts
@@ -31,7 +31,7 @@ at a prompt and for another agent/script (`--json` + exit codes).
31
31
  ```bash
32
32
  arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
33
33
  arkclaw login # browser SSO
34
- arkclaw agents # list the claws you can chat
34
+ arkclaw agents # list the agents you can chat
35
35
  arkclaw chat ci-xxxxxxxx
36
36
  ```
37
37
 
@@ -40,7 +40,8 @@ arkclaw chat ci-xxxxxxxx
40
40
  ## What it can do
41
41
 
42
42
  - **Log in as you** — browser SSO (PKCE), short-lived token, auto-refresh. No AK/SK, no client secret, nothing permanent on disk but a token in your OS keychain.
43
- - **List your agents** — `arkclaw agents` shows the claws you've chatted with (kept locally; add one with `arkclaw chat ci-...`).
43
+ - **List your agents** — `arkclaw agents` shows the agents in your accessible claws (add a claw by chatting it once: `arkclaw chat ci-...`).
44
+ - **Manage agents** — `agents create` / `agents delete` add or remove named agents in a claw, right from the terminal.
44
45
  - **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
45
46
  - **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
46
47
  - **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
@@ -99,14 +100,16 @@ straight to `arkclaw login https://your-space...`. See **[Configuration](#config
99
100
  |---|---|
100
101
  | `arkclaw init [address]` | One-time interactive setup. Saves the address + (only what isn't auto-discovered) the CLI client and STS role to `~/.arkclaw/defaults.json`, so later commands need no env/flags. |
101
102
  | `arkclaw login [space-url]` | Browser SSO login. Uses `init` defaults if you omit the URL. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw. Bare `arkclaw login` re-logs into the previous space. |
102
- | `arkclaw agents` | List the claws you've used from this machine (local history, newest first) + your default claw. Add a claw by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
103
+ | `arkclaw agents` | List the agents in your accessible claws (verified over the ws; inaccessible claws are pruned). Candidate claws = the ones you've used from this machine + your default add one by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
104
+ | `arkclaw agents create` | Create a named agent in a claw: `--name` `--role` `--soul` (+ optional `--description`, repeatable `--skill`). Then `chat <claw> --agent <agentId>`. |
105
+ | `arkclaw agents delete <agent>` | Delete a named agent by agentId (`a-...`) or display name. Asks for confirmation unless `--yes`; the `main` agent is protected. |
103
106
  | `arkclaw chat [TARGET] [MSG]` | Chat. `TARGET` = a claw id (`ci-...`), an agent name (from `agents`), or a profile. With `MSG` → one-shot; without → interactive REPL. `-f` attach files, `-o` write the reply, `--session NAME` name the conversation, `--approve-all` auto-approve tool runs. |
104
107
  | `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
105
108
  | `arkclaw ls` | List the claw's managed workspace files. |
106
109
  | `arkclaw pull <name> [local]` | Download a managed file to local disk. |
107
110
  | `arkclaw push <local> [name]` | Write a local text file into a managed file. |
108
111
  | `arkclaw fanout "<msg>" --clawid ci-a --clawid ci-b` | Same message to many claws in parallel. |
109
- | `arkclaw sessions` | Recent chat sessions on this machine. |
112
+ | `arkclaw sessions [claw]` | An agent's conversations from the server (`--agent` to pick one), newest first — resume with `chat --session <会话ID>`, fork with `--new`. Falls back to the local record when offline / a2a. |
110
113
  | `arkclaw profile save/use/list` | Named snapshots of the session config (multi-space / multi-claw). |
111
114
  | `arkclaw doctor` | Self-check: login freshness, keychain, pool/STS/endpoint reachability. |
112
115
  | `arkclaw schema --json` | Machine-readable command surface (for agents/tooling). |
@@ -7,7 +7,7 @@ at a prompt and for another agent/script (`--json` + exit codes).
7
7
  ```bash
8
8
  arkclaw init # one-time interactive setup (asks only what it can't auto-detect)
9
9
  arkclaw login # browser SSO
10
- arkclaw agents # list the claws you can chat
10
+ arkclaw agents # list the agents you can chat
11
11
  arkclaw chat ci-xxxxxxxx
12
12
  ```
13
13
 
@@ -16,7 +16,8 @@ arkclaw chat ci-xxxxxxxx
16
16
  ## What it can do
17
17
 
18
18
  - **Log in as you** — browser SSO (PKCE), short-lived token, auto-refresh. No AK/SK, no client secret, nothing permanent on disk but a token in your OS keychain.
19
- - **List your agents** — `arkclaw agents` shows the claws you've chatted with (kept locally; add one with `arkclaw chat ci-...`).
19
+ - **List your agents** — `arkclaw agents` shows the agents in your accessible claws (add a claw by chatting it once: `arkclaw chat ci-...`).
20
+ - **Manage agents** — `agents create` / `agents delete` add or remove named agents in a claw, right from the terminal.
20
21
  - **Chat** — interactive REPL or one-shot `-m`, streaming token-by-token, with a live "what's it doing" spinner and tool-event trace.
21
22
  - **Talk to a specific claw** — by id (`chat ci-...`) or by name (`chat 答疑助手`).
22
23
  - **Manage a claw's files** — read/write its managed workspace files (`AGENTS.md`, `SOUL.md`, `MEMORY.md`, …) with `ls` / `pull` / `push`.
@@ -75,14 +76,16 @@ straight to `arkclaw login https://your-space...`. See **[Configuration](#config
75
76
  |---|---|
76
77
  | `arkclaw init [address]` | One-time interactive setup. Saves the address + (only what isn't auto-discovered) the CLI client and STS role to `~/.arkclaw/defaults.json`, so later commands need no env/flags. |
77
78
  | `arkclaw login [space-url]` | Browser SSO login. Uses `init` defaults if you omit the URL. `--transport a2a --endpoint <url>` for an agent endpoint. `--clawid ci-...` sets a default claw. Bare `arkclaw login` re-logs into the previous space. |
78
- | `arkclaw agents` | List the claws you've used from this machine (local history, newest first) + your default claw. Add a claw by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
79
+ | `arkclaw agents` | List the agents in your accessible claws (verified over the ws; inaccessible claws are pruned). Candidate claws = the ones you've used from this machine + your default add one by chatting it once (`arkclaw chat ci-...`). Not a space-wide directory. |
80
+ | `arkclaw agents create` | Create a named agent in a claw: `--name` `--role` `--soul` (+ optional `--description`, repeatable `--skill`). Then `chat <claw> --agent <agentId>`. |
81
+ | `arkclaw agents delete <agent>` | Delete a named agent by agentId (`a-...`) or display name. Asks for confirmation unless `--yes`; the `main` agent is protected. |
79
82
  | `arkclaw chat [TARGET] [MSG]` | Chat. `TARGET` = a claw id (`ci-...`), an agent name (from `agents`), or a profile. With `MSG` → one-shot; without → interactive REPL. `-f` attach files, `-o` write the reply, `--session NAME` name the conversation, `--approve-all` auto-approve tool runs. |
80
83
  | `arkclaw <name>` | Shortcut: `arkclaw 答疑助手` ≡ `arkclaw chat 答疑助手`. |
81
84
  | `arkclaw ls` | List the claw's managed workspace files. |
82
85
  | `arkclaw pull <name> [local]` | Download a managed file to local disk. |
83
86
  | `arkclaw push <local> [name]` | Write a local text file into a managed file. |
84
87
  | `arkclaw fanout "<msg>" --clawid ci-a --clawid ci-b` | Same message to many claws in parallel. |
85
- | `arkclaw sessions` | Recent chat sessions on this machine. |
88
+ | `arkclaw sessions [claw]` | An agent's conversations from the server (`--agent` to pick one), newest first — resume with `chat --session <会话ID>`, fork with `--new`. Falls back to the local record when offline / a2a. |
86
89
  | `arkclaw profile save/use/list` | Named snapshots of the session config (multi-space / multi-claw). |
87
90
  | `arkclaw doctor` | Self-check: login freshness, keychain, pool/STS/endpoint reachability. |
88
91
  | `arkclaw schema --json` | Machine-readable command surface (for agents/tooling). |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arkclaw-webchat-cli"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "CLI to chat with an ArkClaw EE space's Claw over enterprise SSO — zero permanent AK/SK."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -161,11 +161,20 @@ def chat(
161
161
  message_opt: str | None = typer.Option(
162
162
  None, "-m", "--message", help="One-shot message (same as the positional argument)."
163
163
  ),
164
+ agent: str = typer.Option(
165
+ "main",
166
+ "--agent",
167
+ help="Which agent in the claw to talk to (its agentId; see `arkclaw agents`). "
168
+ "Defaults to the claw's main agent.",
169
+ ),
164
170
  session: str | None = typer.Option(
165
171
  None,
166
172
  "--session",
167
- help="Named server-side session (conversation thread). Defaults to the "
168
- "claw's main session; sessions survive CLI restarts.",
173
+ help="Resume a specific conversation by its 会话ID (see `arkclaw sessions`). "
174
+ "Omit to continue the agent's main session.",
175
+ ),
176
+ new: bool = typer.Option(
177
+ False, "--new", help="Start a fresh conversation instead of continuing the last one."
169
178
  ),
170
179
  approve_all: bool = typer.Option(
171
180
  False,
@@ -192,6 +201,8 @@ def chat(
192
201
  target=target,
193
202
  message=message_opt or message,
194
203
  session=session,
204
+ agent=agent,
205
+ new=new,
195
206
  approve_all=approve_all,
196
207
  files=list(file) or None,
197
208
  output=output,
@@ -200,10 +211,17 @@ def chat(
200
211
 
201
212
 
202
213
  @app.command()
203
- def sessions(json_mode: bool = _json_opt()) -> None:
204
- """List chat sessions this machine has used (most recent first)."""
214
+ def sessions(
215
+ claw: str | None = typer.Argument(
216
+ None, metavar="[CLAW]", help="Claw id (ci-...). Omit to use your default claw."
217
+ ),
218
+ agent: str = typer.Option("main", "--agent", help="Which agent's conversations to list."),
219
+ json_mode: bool = _json_opt(),
220
+ ) -> None:
221
+ """List an agent's conversations from the server (newest first). Resume one
222
+ with `arkclaw chat <claw> --session <会话ID>`, or start fresh with `--new`."""
205
223
  emitter = Emitter(json_mode=json_mode)
206
- _run(emitter, lambda: flows.do_sessions(emitter))
224
+ _run(emitter, lambda: flows.do_sessions(emitter, clawid=claw, agent=agent))
207
225
 
208
226
 
209
227
  @app.command(name="ls")
@@ -241,14 +259,76 @@ def push(
241
259
  _run(emitter, lambda: flows.do_push(local, remote, emitter, clawid=clawid))
242
260
 
243
261
 
244
- @app.command()
245
- def agents(json_mode: bool = _json_opt()) -> None:
246
- """List the agents/claws you can chat with (openclaw: your claws, space
247
- auto-discovered; a2a: the agent at the endpoint). Then `chat <id-or-name>`."""
262
+ agents_app = typer.Typer()
263
+ app.add_typer(agents_app, name="agents")
264
+
265
+
266
+ @agents_app.callback(invoke_without_command=True)
267
+ def agents(ctx: typer.Context, json_mode: bool = _json_opt()) -> None:
268
+ """List the agents you can chat with (openclaw: the agents in your accessible
269
+ claws; a2a: the agent at the endpoint). Then `chat <claw-or-name>`."""
270
+ if ctx.invoked_subcommand is not None:
271
+ # `agents --json create …` must honor the group-level flag — stash it
272
+ # for the subcommand (click contexts inherit obj), never drop it.
273
+ ctx.obj = {"json": json_mode}
274
+ return
248
275
  emitter = Emitter(json_mode=json_mode)
249
276
  _run(emitter, lambda: flows.do_agents(emitter))
250
277
 
251
278
 
279
+ def _group_json(ctx: typer.Context, json_mode: bool) -> bool:
280
+ """A subcommand's effective --json: its own flag OR the group-level one."""
281
+ return json_mode or bool((ctx.obj or {}).get("json"))
282
+
283
+
284
+ @agents_app.command("create")
285
+ def agents_create(
286
+ ctx: typer.Context,
287
+ name: str = typer.Option(..., "--name", help="Agent display name."),
288
+ role: str = typer.Option(..., "--role", help="One-line role (what this agent does)."),
289
+ soul: str = typer.Option(
290
+ ..., "--soul", help="Persona / system instructions (the agent's SOUL.md)."
291
+ ),
292
+ description: str | None = typer.Option(None, "--description", help="Optional description."),
293
+ skill: list[str] = typer.Option(
294
+ [], "--skill", help="Skill to enable (repeat for several)."
295
+ ),
296
+ clawid: str | None = typer.Option(
297
+ None, "--clawid", help="Claw instance id (ci-...). Omit to use your default claw."
298
+ ),
299
+ json_mode: bool = _json_opt(),
300
+ ) -> None:
301
+ """Create a named agent in a claw. Then `arkclaw chat <claw> --agent <agentId>`."""
302
+ emitter = Emitter(json_mode=_group_json(ctx, json_mode))
303
+ _run(
304
+ emitter,
305
+ lambda: flows.do_agents_create(
306
+ emitter,
307
+ name=name,
308
+ role=role,
309
+ soul=soul,
310
+ description=description,
311
+ skills=list(skill),
312
+ clawid=clawid,
313
+ ),
314
+ )
315
+
316
+
317
+ @agents_app.command("delete")
318
+ def agents_delete(
319
+ ctx: typer.Context,
320
+ agent: str = typer.Argument(..., help="agentId (a-...) or display name to delete."),
321
+ clawid: str | None = typer.Option(
322
+ None, "--clawid", help="Claw instance id (ci-...). Omit to use your default claw."
323
+ ),
324
+ yes: bool = typer.Option(False, "--yes", help="Skip the confirmation prompt."),
325
+ json_mode: bool = _json_opt(),
326
+ ) -> None:
327
+ """Delete a named agent from a claw (asks for confirmation unless --yes)."""
328
+ emitter = Emitter(json_mode=_group_json(ctx, json_mode))
329
+ _run(emitter, lambda: flows.do_agents_delete(emitter, agent, clawid=clawid, yes=yes))
330
+
331
+
252
332
  @app.command()
253
333
  def fanout(
254
334
  message: str = typer.Argument(..., help="The message to send to every claw."),
@@ -394,6 +474,8 @@ def _schema_data() -> dict:
394
474
  for name, cmd in sorted(getattr(root, "commands", {}).items()):
395
475
  subs = getattr(cmd, "commands", None)
396
476
  if subs:
477
+ if getattr(cmd, "invoke_without_command", False):
478
+ commands.append(info(name, cmd)) # the bare group is a command too
397
479
  commands.extend(info(f"{name} {s}", c) for s, c in sorted(subs.items()))
398
480
  else:
399
481
  commands.append(info(name, cmd))
@@ -20,6 +20,7 @@ import signal
20
20
  import sys
21
21
  import time
22
22
  import urllib.parse
23
+ import uuid
23
24
  from typing import Any
24
25
 
25
26
  from ee_claw import config as config_mod
@@ -51,9 +52,9 @@ from ee_claw.identity import (
51
52
  read_chrome_recent_claw,
52
53
  )
53
54
  from ee_claw.oauth import OAuthClient
54
- from ee_claw.output import Emitter
55
+ from ee_claw.output import Emitter, sanitize
55
56
  from ee_claw.providers import ProviderContext, resolve_token_source
56
- from ee_claw.sts import assume_role_with_oidc, get_chat_token
57
+ from ee_claw.sts import assume_role_with_oidc
57
58
  from ee_claw.transport.a2a import A2ATransport, agent_card
58
59
  from ee_claw.transport.base import Approver, Attachment, Transport, TurnEvent, TurnResult
59
60
  from ee_claw.transport.openclaw import OpenClawTransport, session_key_for
@@ -116,35 +117,77 @@ def _user_identity(id_token: str | None) -> dict[str, str]:
116
117
  return ident
117
118
 
118
119
 
119
- def _probe_claw_access(
120
- cfg: SessionConfig, id_token: str, claw_ids: list[str]
121
- ) -> tuple[set[str], set[str]]:
122
- """Which of ``claw_ids`` the current user can ACTUALLY chat — checked live by
123
- minting a (throwaway) ChatToken for each, the only per-user check the CLI's
124
- role can do. Returns (accessible, denied). A transient/non-permission error
125
- keeps the claw (don't hide it on a network blip). Raises ArkclawError if
126
- creds can't be obtained at all (caller falls back to the unchecked list)."""
120
+ def _agent_display_name(agent: dict[str, Any]) -> str:
121
+ """Friendly name for an agent. The main agent often comes back named "main";
122
+ show a friendly label instead of leaking the internal id (mirrors the web)."""
123
+ name = str(agent.get("name") or "").strip()
124
+ agent_id = str(agent.get("agentId") or "")
125
+ is_main = bool(agent.get("default")) or agent_id == "main"
126
+ if is_main and (not name or name == "main"):
127
+ return "ArkClaw 智能助手"
128
+ return name or agent_id or "(未命名)"
129
+
130
+
131
+ def _list_agents_for_claws(
132
+ cfg: SessionConfig, creds: dict[str, str], identity: dict[str, str], claw_ids: list[str]
133
+ ) -> dict[str, tuple[str, list[dict[str, Any]]]]:
134
+ """For each claw, list its agents over the ws (``arkclaw.team.agent.list``) —
135
+ which also proves access (the ChatToken mint denies non-owners). Returns
136
+ ``clawid → (status, agents)``, status ∈ {"ok","denied","error"}. Parallel."""
127
137
  from concurrent.futures import ThreadPoolExecutor
128
138
 
129
- creds = _openclaw_creds(cfg, id_token) # may raise (offline / expired / no creds)
130
- identity = _user_identity(id_token)
131
139
  region = str(cfg.region)
132
140
 
133
- def check(cid: str) -> tuple[str, bool]:
141
+ def one(clawid: str) -> tuple[str, str, list[dict[str, Any]]]:
142
+ t = OpenClawTransport(clawid=clawid, region=region, creds=creds, identity=identity)
134
143
  try:
135
- get_chat_token(cid, region, creds, identity=identity)
136
- return cid, True
144
+ return clawid, "ok", asyncio.run(t.list_agents())
137
145
  except ClawAccessDeniedError:
138
- return cid, False
146
+ return clawid, "denied", []
139
147
  except ArkclawError:
140
- return cid, True # transient/other — keep, don't silently drop
148
+ return clawid, "error", [] # transient — keep the claw, mark unknown
141
149
 
142
- accessible: set[str] = set()
143
- denied: set[str] = set()
150
+ out: dict[str, tuple[str, list[dict[str, Any]]]] = {}
144
151
  with ThreadPoolExecutor(max_workers=min(8, max(1, len(claw_ids)))) as ex:
145
- for cid, ok in ex.map(check, claw_ids):
146
- (accessible if ok else denied).add(cid)
147
- return accessible, denied
152
+ for clawid, status, agents in ex.map(one, claw_ids):
153
+ out[clawid] = (status, agents)
154
+ return out
155
+
156
+
157
+ def _resolve_session_key(
158
+ cfg: SessionConfig, id_token: str, clawid: str, agent: str, session: str | None, new: bool
159
+ ) -> str | None:
160
+ """Compute the OpenClaw sessionKey ``agent:<agentId>:<label>`` for a chat:
161
+
162
+ * ``--new`` → a fresh ``agent:<agent>:cli-<uuid>`` (new conversation)
163
+ * ``--session <id>`` → resume: a full ``agent:…`` key passes through; a short
164
+ sessionId is resolved to its key via ``sessions.list``
165
+ * neither, agent=main → ``None`` (the default ``agent:main:main`` session)
166
+ * neither, other agent→ ``agent:<agent>:main``
167
+ """
168
+ agent = agent or "main"
169
+ if new:
170
+ return f"agent:{agent}:cli-{uuid.uuid4()}"
171
+ if not session:
172
+ return None if agent == "main" else f"agent:{agent}:main"
173
+ if session.startswith("agent:"):
174
+ return session # already a full sessionKey
175
+ # A hex token is a 会话ID copied from `arkclaw sessions` → resolve to its key.
176
+ if re.fullmatch(r"[0-9a-f]{6,}", session):
177
+ creds = _openclaw_creds(cfg, id_token)
178
+ t = OpenClawTransport(
179
+ clawid=clawid, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
180
+ )
181
+ for s in asyncio.run(t.list_sessions(agent)):
182
+ sid = str(s.get("sessionId") or "")
183
+ if sid and (sid == session or sid.startswith(session)):
184
+ return str(s.get("key") or f"agent:{agent}:{session}")
185
+ raise ValidationError(
186
+ f"会话 {session!r} 不存在。",
187
+ hint="用 `arkclaw sessions` 查看现有会话,或加 `--new` 开新会话。",
188
+ )
189
+ # Otherwise a human-named session label (created/resumed by this key).
190
+ return f"agent:{agent}:{session}"
148
191
 
149
192
 
150
193
  def _normalize(url: str) -> tuple[str, str]:
@@ -783,6 +826,8 @@ def do_chat(
783
826
  target: str | None = None,
784
827
  message: str | None = None,
785
828
  session: str | None = None,
829
+ agent: str = "main",
830
+ new: bool = False,
786
831
  approve_all: bool = False,
787
832
  files: list[str] | None = None,
788
833
  output: str | None = None,
@@ -838,6 +883,12 @@ def do_chat(
838
883
  )
839
884
 
840
885
  approver = _build_approver(emitter, approve_all=approve_all, interactive=interactive)
886
+ # Resolve agent + session → a concrete sessionKey (openclaw only; a2a uses
887
+ # the session name as the context label).
888
+ if cfg.transport == "openclaw":
889
+ effective_claw = clawid or cfg.claw
890
+ if effective_claw:
891
+ session = _resolve_session_key(cfg, id_token, effective_claw, agent, session, new)
841
892
  target, transport = _target_transport(
842
893
  cfg, clawid, id_token, session=session, approver=approver
843
894
  )
@@ -879,8 +930,9 @@ def do_chat(
879
930
  pass
880
931
  emitter.line(
881
932
  f"ArkClaw chat · {target}"
933
+ + (f" · agent {agent}" if agent and agent != "main" else "")
934
+ + (f" · 会话 {session.split(':')[-1][:16]}" if session else "")
882
935
  + (f" · 空间 {cfg.space}" if cfg.space and cfg.space != target else "")
883
- + (f" · 会话 {session}" if session else "")
884
936
  + " (Ctrl+C 退出)"
885
937
  )
886
938
  while True:
@@ -911,11 +963,54 @@ def do_chat(
911
963
  emitter.line("")
912
964
 
913
965
 
914
- def do_sessions(emitter: Emitter) -> dict[str, Any]:
915
- """``arkclaw sessions``: the local view of server-side sessions (which
916
- space/claw/session tuples this machine has talked to). Server-side
917
- listing + replay land with gate G2."""
918
- rows = config_mod.list_sessions()
966
+ def do_sessions(
967
+ emitter: Emitter, clawid: str | None = None, agent: str = "main"
968
+ ) -> dict[str, Any]:
969
+ """``arkclaw sessions [<claw>] [--agent <id>]``: the conversations of an agent
970
+ in a claw, from the server (ws ``sessions.list``) — newest first. Resume one
971
+ with ``arkclaw chat <claw> --session <会话ID>``. Falls back to the local
972
+ record for a2a / when the claw can't be reached."""
973
+ cfg = config_mod.load_config()
974
+ target = (clawid or (cfg.claw if cfg else None)) if cfg else None
975
+ if cfg and cfg.space and cfg.transport == "openclaw" and target:
976
+ try:
977
+ _, id_token = _load_session(cfg)
978
+ creds = _openclaw_creds(cfg, id_token)
979
+ t = OpenClawTransport(
980
+ clawid=target, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
981
+ )
982
+ sessions = asyncio.run(t.list_sessions(agent))
983
+ sessions.sort(key=lambda s: float(s.get("updatedAt") or 0), reverse=True)
984
+ emitter.line(f"★ claw {target} · agent {agent} · 空间 {cfg.space}")
985
+ if sessions:
986
+ emitter.table(
987
+ ["标题", "上次活动", "会话ID", "状态"],
988
+ [
989
+ [
990
+ (str(s.get("derivedTitle") or "(无标题)"))[:48],
991
+ _ago_ms(s.get("updatedAt")),
992
+ str(s.get("sessionId") or "")[:8],
993
+ str(s.get("status") or "—"),
994
+ ]
995
+ for s in sessions
996
+ ],
997
+ )
998
+ emitter.line(" 续聊: arkclaw chat <claw> --session <会话ID>;新开: --new")
999
+ else:
1000
+ emitter.line(" 该 agent 还没有会话。`arkclaw chat` 开始第一段对话。")
1001
+ return {
1002
+ "claw": target, "agent": agent,
1003
+ "sessions": [
1004
+ {"sessionId": s.get("sessionId"), "key": s.get("key"),
1005
+ "title": s.get("derivedTitle"), "updatedAt": s.get("updatedAt"),
1006
+ "status": s.get("status")}
1007
+ for s in sessions
1008
+ ],
1009
+ }
1010
+ except ArkclawError as e:
1011
+ emitter.line(f" ⚠ 无法从服务端列会话({e.code}),回退本地记录:")
1012
+
1013
+ rows = config_mod.list_sessions() # a2a / not-logged-in / no-claw / server-error fallback
919
1014
  if rows:
920
1015
  emitter.table(
921
1016
  ["会话", "目标", "空间", "最近使用"],
@@ -964,13 +1059,15 @@ def _ago(ts: object) -> str:
964
1059
 
965
1060
 
966
1061
  def do_agents(emitter: Emitter) -> dict[str, Any]:
967
- """``arkclaw agents``: the agents/claws you can chat with.
1062
+ """``arkclaw agents``: the agents you can chat with.
968
1063
 
969
- openclaw → the claws YOU have used from this machine (kept locally, newest
970
- first), plus your default claw. This is deliberately NOT a server-side
971
- enumeration of the space that API returns every member's claws to anyone
972
- (no per-user scoping is reachable by the CLI's role), so we don't call it.
973
- You add a claw to this list by chatting it once: ``arkclaw chat ci-...``.
1064
+ openclaw → the AGENTS hosted in your accessible claws. The candidate claws
1065
+ are the ones YOU have used from this machine (kept locally, newest first)
1066
+ plus your default; for each we list its agents over the ws
1067
+ (``arkclaw.team.agent.list``), which also verifies access — inaccessible
1068
+ claws are pruned. This is NOT a space-wide server enumeration (not per-user
1069
+ scoped for the CLI's role). You add a claw by chatting it once: ``arkclaw
1070
+ chat ci-...``.
974
1071
 
975
1072
  a2a → the agent at the endpoint (its card). Profiles live under ``arkclaw
976
1073
  profile``, full conversation history under ``arkclaw sessions``."""
@@ -1030,11 +1127,11 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1030
1127
  )
1031
1128
  elif cfg.transport == "openclaw":
1032
1129
  emitter.line(f"★ 当前登录(openclaw)· 空间 {cfg.space}")
1033
- # Candidates = the claws YOU have used from this machine (local history) +
1034
- # your default NOT a server enumeration (the space-wide list isn't
1035
- # per-user scoped for the CLI's role). We then VERIFY access to each by
1036
- # minting a throwaway ChatToken, so only claws you can actually chat are
1037
- # shown; denied ones are pruned from history (and cleared as default).
1130
+ # The chat-able units are AGENTS, and one claw hosts several. Candidates
1131
+ # are the claws YOU have used from this machine (local history) + your
1132
+ # default; for each we list its agents over the ws (which also proves
1133
+ # access). Inaccessible claws are pruned from history (+ cleared as
1134
+ # default). NOT a space-wide server enumeration (not per-user scoped).
1038
1135
  seen: dict[str, float] = {}
1039
1136
  for s in config_mod.list_sessions():
1040
1137
  cid = s.get("claw")
@@ -1045,43 +1142,56 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1045
1142
  seen[str(cfg.claw)] = 0.0 # the default claw, even if not chatted yet
1046
1143
 
1047
1144
  claw_ids = sorted(seen, key=lambda c: seen[c], reverse=True)
1145
+ rows: list[list[str]] = []
1146
+ agents_data: list[dict[str, Any]] = []
1048
1147
  checked = False
1049
1148
  if claw_ids:
1050
1149
  if not emitter.json:
1051
- emitter.line(" 核对访问权限中…")
1150
+ emitter.line(" 列出 agents…")
1052
1151
  try:
1053
1152
  _, id_token = _load_session(cfg)
1054
- accessible, denied = _probe_claw_access(cfg, id_token, claw_ids)
1153
+ creds = _openclaw_creds(cfg, id_token)
1154
+ identity = _user_identity(id_token)
1155
+ results = _list_agents_for_claws(cfg, creds, identity, claw_ids)
1055
1156
  checked = True
1056
- for cid in denied:
1057
- config_mod.forget_claw(str(cfg.space), cid) # stop listing it
1058
- if cfg.claw == cid:
1059
- config_mod.save_config(dataclasses.replace(cfg, claw=None))
1060
- cfg = config_mod.load_config() or cfg
1061
- current["default_claw"] = cfg.claw
1062
- claw_ids = [c for c in claw_ids if c in accessible]
1157
+ for clawid in claw_ids:
1158
+ status, agents = results[clawid]
1159
+ if status == "denied":
1160
+ config_mod.forget_claw(str(cfg.space), clawid) # stop listing it
1161
+ if cfg.claw == clawid:
1162
+ config_mod.save_config(dataclasses.replace(cfg, claw=None))
1163
+ cfg = config_mod.load_config() or cfg
1164
+ current["default_claw"] = cfg.claw
1165
+ continue
1166
+ if status == "error" or not agents:
1167
+ rows.append(["(无法列出 agent)", "—", clawid, "★" if clawid == cfg.claw else ""])
1168
+ continue
1169
+ for a in agents:
1170
+ aid = str(a.get("agentId") or "")
1171
+ is_main = bool(a.get("default")) or aid == "main"
1172
+ rows.append([
1173
+ _agent_display_name(a), aid or "—", clawid,
1174
+ "★" if (clawid == cfg.claw and is_main) else "",
1175
+ ])
1176
+ agents_data.append({
1177
+ "clawId": clawid, "agentId": aid, "name": _agent_display_name(a),
1178
+ "description": a.get("description"), "default": is_main,
1179
+ })
1063
1180
  except ArkclawError:
1064
- checked = False # offline / expired / no creds → show unchecked
1181
+ checked = False # offline / expired / no creds → can't list agents
1065
1182
 
1066
- claws = [{"ClawInstanceId": cid, "last_used": seen[cid]} for cid in claw_ids]
1067
- current["claws"] = claws
1183
+ current["agents"] = agents_data
1068
1184
  current["source"] = "verified" if checked else "local-history-unchecked"
1069
- if claws:
1185
+ if checked and rows:
1186
+ emitter.table(["Agent", "agentId", "所属 Claw", "默认"], rows)
1187
+ elif checked:
1188
+ emitter.line(" 本空间没有你可访问的 claw / agent(历史里的都已无权或不存在)。")
1189
+ elif claw_ids:
1190
+ emitter.line(" ⚠ 未能连接核对(离线/会话过期),无法列出 agent;以下为本地历史的 claw:")
1070
1191
  emitter.table(
1071
- ["Claw(可 chat)", "上次使用", "默认"],
1072
- [
1073
- [
1074
- str(c["ClawInstanceId"]),
1075
- _ago(c["last_used"]) if c["last_used"] else "—",
1076
- "★" if c["ClawInstanceId"] == cfg.claw else "",
1077
- ]
1078
- for c in claws
1079
- ],
1192
+ ["Claw", "上次使用"],
1193
+ [[c, _ago(seen[c]) if seen[c] else "—"] for c in claw_ids],
1080
1194
  )
1081
- if not checked:
1082
- emitter.line(" ⚠ 未能核对权限(离线/会话过期),以上为本地历史,可能含你无权访问的。")
1083
- elif checked:
1084
- emitter.line(" 你在本空间没有可访问的 claw(历史里的都已无权或不存在)。")
1085
1195
  else:
1086
1196
  emitter.line(" 还没用过任何 claw。用 `arkclaw chat ci-xxxx` 开始,之后这里会记住它。")
1087
1197
 
@@ -1091,15 +1201,14 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1091
1201
  return {"current": current}
1092
1202
 
1093
1203
 
1094
- def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str]:
1095
- """Build an OpenClawTransport for file transfer (openclaw-only). Returns
1096
- (transport, clawid)."""
1204
+ def _openclaw_claw_transport(
1205
+ clawid: str | None, *, feature: str, hint: str
1206
+ ) -> tuple[OpenClawTransport, str]:
1207
+ """Build an OpenClawTransport against the given (or default) claw for an
1208
+ openclaw-only feature. Returns (transport, clawid)."""
1097
1209
  cfg, id_token = _load_session()
1098
1210
  if cfg.transport != "openclaw":
1099
- raise ValidationError(
1100
- "文件传输目前仅支持 openclaw transport(claw 工作区文件)。",
1101
- hint="A2A agent 没有可直传的工作区文件系统。",
1102
- )
1211
+ raise ValidationError(f"{feature}目前仅支持 openclaw transport。", hint=hint)
1103
1212
  clawid = clawid or cfg.claw
1104
1213
  if not clawid:
1105
1214
  raise NoClawError("未指定 claw。", hint="加 --clawid <ci-...>。")
@@ -1111,6 +1220,104 @@ def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str
1111
1220
  ), clawid
1112
1221
 
1113
1222
 
1223
+ def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str]:
1224
+ """Build an OpenClawTransport for file transfer (openclaw-only). Returns
1225
+ (transport, clawid)."""
1226
+ return _openclaw_claw_transport(
1227
+ clawid, feature="文件传输", hint="A2A agent 没有可直传的工作区文件系统。"
1228
+ )
1229
+
1230
+
1231
+ def _agent_mgmt_transport(clawid: str | None) -> tuple[OpenClawTransport, str]:
1232
+ return _openclaw_claw_transport(
1233
+ clawid, feature="agent 管理", hint="A2A 模式连接的是单个 agent 端点,没有可管理的 claw。"
1234
+ )
1235
+
1236
+
1237
+ def do_agents_create(
1238
+ emitter: Emitter,
1239
+ *,
1240
+ name: str,
1241
+ role: str,
1242
+ soul: str,
1243
+ description: str | None = None,
1244
+ skills: list[str] | None = None,
1245
+ clawid: str | None = None,
1246
+ ) -> dict[str, Any]:
1247
+ """``arkclaw agents create``: create a named agent in a claw (ws RPC
1248
+ ``arkclaw.team.agent.create``; live-calibrated 2026-06-10). The server
1249
+ requires name+role+soul; skills may be empty."""
1250
+ for label, value in (("--name", name), ("--role", role), ("--soul", soul)):
1251
+ if not value.strip():
1252
+ raise ValidationError(f"{label} 不能为空。")
1253
+ if name.strip().startswith("a-") or name.strip() == "main":
1254
+ raise ValidationError(
1255
+ f"--name 不能是 {name.strip()!r} — 会与 agentId 命名空间(a-…/main)冲突。"
1256
+ )
1257
+ transport, clawid = _agent_mgmt_transport(clawid)
1258
+ raw = asyncio.run(
1259
+ transport.create_agent(
1260
+ name=name, role=role, soul=soul, description=description, skills=skills or []
1261
+ )
1262
+ )
1263
+ agent = redact_obj(raw)
1264
+ assert isinstance(agent, dict) # redact_obj preserves the dict shape
1265
+ aid = str(agent.get("agentId") or "")
1266
+ emitter.line(f"✓ 已创建 agent {agent.get('name') or name}({aid or '?'})· claw {clawid}")
1267
+ if aid:
1268
+ emitter.line(f" 开聊: arkclaw chat {clawid} --agent {aid}")
1269
+ return {"claw": clawid, "agent": agent}
1270
+
1271
+
1272
+ def do_agents_delete(
1273
+ emitter: Emitter, agent: str, *, clawid: str | None = None, yes: bool = False
1274
+ ) -> dict[str, Any]:
1275
+ """``arkclaw agents delete``: remove a named agent from a claw (ws RPC
1276
+ ``arkclaw.team.agent.delete``). Accepts an agentId (``a-…``) or the agent's
1277
+ display name; either way the target is resolved against the claw's live
1278
+ agent list first — so a typo'd id can't delete anything, an ``a-…``-looking
1279
+ display name can't shadow another agent's id, and the claw's default agent
1280
+ (``main`` or ``default: true``) stays protected."""
1281
+ if agent == "main": # fail fast — no network needed to refuse this
1282
+ raise ValidationError("默认(main)agent 不能删除。")
1283
+ transport, clawid = _agent_mgmt_transport(clawid)
1284
+ agents = asyncio.run(transport.list_agents())
1285
+ matches = [a for a in agents if str(a.get("agentId") or "") == agent]
1286
+ if not matches: # not an id → try the display name
1287
+ matches = [a for a in agents if str(a.get("name") or "") == agent]
1288
+ if len(matches) > 1:
1289
+ ids = ", ".join(str(a.get("agentId") or "?") for a in matches)
1290
+ raise ValidationError(
1291
+ f"名称 {agent!r} 对应多个 agent({ids}),请直接用 agentId。"
1292
+ )
1293
+ if not matches:
1294
+ raise ValidationError(
1295
+ f"claw {clawid} 里没有 agentId 或名称为 {agent!r} 的 agent。",
1296
+ hint="用 `arkclaw agents` 查看 agentId 与名称。",
1297
+ )
1298
+ target = matches[0]
1299
+ agent_id = str(target.get("agentId") or "")
1300
+ if not agent_id:
1301
+ raise ValidationError(f"服务端没有给出 {agent!r} 的 agentId,无法安全删除。")
1302
+ if agent_id == "main" or bool(target.get("default")):
1303
+ raise ValidationError("默认(main)agent 不能删除。")
1304
+ if not yes:
1305
+ if emitter.json or not sys.stdin.isatty():
1306
+ raise ValidationError("headless 删除必须显式 --yes。")
1307
+ # agent_id is upstream content: sanitize before it touches the TTY —
1308
+ # input() bypasses Emitter.line's control-char stripping.
1309
+ prompt = sanitize(f"确认删除 claw {clawid} 的 agent {agent_id}? [y/N] ")
1310
+ answer = input(prompt).strip().lower()
1311
+ if answer not in ("y", "yes"):
1312
+ emitter.line("已取消。")
1313
+ return {"cancelled": True}
1314
+ result = redact_obj(asyncio.run(transport.delete_agent(agent_id)))
1315
+ if isinstance(result, dict) and result.get("success") is False:
1316
+ raise NetworkError(f"claw 未确认删除 agent {agent_id}(success=false)。")
1317
+ emitter.line(f"✓ 已删除 agent {agent_id} · claw {clawid}")
1318
+ return {"claw": clawid, "agentId": agent_id, "result": result}
1319
+
1320
+
1114
1321
  def do_ls(emitter: Emitter, *, clawid: str | None = None) -> dict[str, Any]:
1115
1322
  """``arkclaw ls``: list files in the claw workspace."""
1116
1323
  transport, clawid = _openclaw_file_transport(clawid)
@@ -1316,7 +1523,7 @@ def do_claw(
1316
1523
  if op == "delete" and not yes:
1317
1524
  if emitter.json or not sys.stdin.isatty():
1318
1525
  raise ValidationError("headless 删除必须显式 --yes。")
1319
- answer = input(f"确认删除 claw {clawid}? [y/N] ").strip().lower()
1526
+ answer = input(sanitize(f"确认删除 claw {clawid}? [y/N] ")).strip().lower()
1320
1527
  if answer not in ("y", "yes"):
1321
1528
  emitter.line("已取消。")
1322
1529
  return {"cancelled": True}
@@ -451,9 +451,10 @@ class OpenClawTransport:
451
451
  # agents.files.get {agentId, name} → {file:{name,path,size,content}}
452
452
  # agents.files.set {agentId, name, content} → write
453
453
 
454
- async def _request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
455
- """connect (hello-ok) → one control request → its res payload. Errors
456
- are redacted; the token-bearing URL is never echoed."""
454
+ async def _request(self, method: str, params: dict[str, Any]) -> Any:
455
+ """connect (hello-ok) → one control request → its res payload (dict or
456
+ list, depending on the method). Errors are redacted; the token-bearing
457
+ URL is never echoed."""
457
458
  chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds, identity=self.identity)
458
459
  url = (
459
460
  f"wss://{endpoint}/?chatToken={urllib.parse.quote(chat_token)}"
@@ -510,3 +511,60 @@ class OpenClawTransport:
510
511
  return await self._request(
511
512
  "agents.files.set", {"agentId": agent_id, "name": name, "content": content}
512
513
  )
514
+
515
+ async def list_agents(self) -> list[dict[str, Any]]:
516
+ """The agents living in this claw (ws RPC ``arkclaw.team.agent.list``).
517
+ Each item: ``{agentId, name, description?, default?}``. Raises
518
+ ClawAccessDeniedError (via the ChatToken mint) if the user can't access
519
+ this claw."""
520
+ payload = await self._request("arkclaw.team.agent.list", {})
521
+ if isinstance(payload, list):
522
+ return [a for a in payload if isinstance(a, dict)]
523
+ if isinstance(payload, dict): # some gateways wrap the array
524
+ for k in ("data", "agents", "list", "items"):
525
+ v = payload.get(k)
526
+ if isinstance(v, list):
527
+ return [a for a in v if isinstance(a, dict)]
528
+ return []
529
+
530
+ async def create_agent(
531
+ self,
532
+ *,
533
+ name: str,
534
+ role: str,
535
+ soul: str,
536
+ description: str | None = None,
537
+ skills: Sequence[str] = (),
538
+ ) -> dict[str, Any]:
539
+ """Create a named agent in this claw (ws RPC ``arkclaw.team.agent.create``).
540
+ The server requires name + role + soul + skills (array, may be empty);
541
+ returns the created agent ``{agentId, name, role, soul, …}``."""
542
+ params: dict[str, Any] = {
543
+ "name": name,
544
+ "role": role,
545
+ "soul": soul,
546
+ "skills": list(skills),
547
+ }
548
+ if description:
549
+ params["description"] = description
550
+ payload = await self._request("arkclaw.team.agent.create", params)
551
+ return payload if isinstance(payload, dict) else {}
552
+
553
+ async def delete_agent(self, agent_id: str) -> dict[str, Any]:
554
+ """Delete an agent from this claw (ws RPC ``arkclaw.team.agent.delete``).
555
+ Returns the server ack (``{success: true}``)."""
556
+ payload = await self._request("arkclaw.team.agent.delete", {"agentId": agent_id})
557
+ return payload if isinstance(payload, dict) else {}
558
+
559
+ async def list_sessions(self, agent_id: str = "main") -> list[dict[str, Any]]:
560
+ """An agent's conversations (ws RPC ``sessions.list``). Each item:
561
+ ``{key, sessionId, derivedTitle?, updatedAt, status, ...}`` — ``key`` is
562
+ the sessionKey to resume with."""
563
+ payload = await self._request(
564
+ "sessions.list", {"agentId": agent_id, "includeDerivedTitles": True}
565
+ )
566
+ if isinstance(payload, dict):
567
+ sessions = payload.get("sessions")
568
+ if isinstance(sessions, list):
569
+ return [s for s in sessions if isinstance(s, dict)]
570
+ return []