arkclaw-webchat-cli 0.5.1__tar.gz → 0.5.3__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.5.1 → arkclaw_webchat_cli-0.5.3}/PKG-INFO +1 -1
  2. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/pyproject.toml +1 -1
  3. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/config.py +18 -0
  4. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/errors.py +7 -0
  5. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/flows.py +21 -3
  6. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/sts.py +2 -2
  7. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/openclaw.py +43 -4
  8. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/.gitignore +0 -0
  9. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/README.md +0 -0
  10. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/__init__.py +0 -0
  11. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/attachments.py +0 -0
  12. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/cli.py +0 -0
  13. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/control.py +0 -0
  14. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/core.py +0 -0
  15. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/doctor.py +0 -0
  16. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/identity.py +0 -0
  17. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/oauth.py +0 -0
  18. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/output.py +0 -0
  19. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/policy.py +0 -0
  20. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/providers.py +0 -0
  21. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/secrets_store.py +0 -0
  22. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/__init__.py +0 -0
  23. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/a2a.py +0 -0
  24. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/base.py +0 -0
  25. {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/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.5.1
3
+ Version: 0.5.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arkclaw-webchat-cli"
7
- version = "0.5.1"
7
+ version = "0.5.3"
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"
@@ -126,6 +126,24 @@ def get_session(space: str, claw: str, session: str) -> dict[str, object] | None
126
126
  return entry if isinstance(entry, dict) else None
127
127
 
128
128
 
129
+ def forget_claw(space: str, claw: str) -> bool:
130
+ """Drop ALL recorded sessions for a (space, claw) — used to self-heal the
131
+ `agents` list when a claw turns out to be inaccessible. Returns True if any
132
+ entry was removed."""
133
+ data = _load_sessions()
134
+ prefix = f"{space}|{claw}|"
135
+ doomed = [k for k in data if k.startswith(prefix)]
136
+ if not doomed:
137
+ return False
138
+ for k in doomed:
139
+ del data[k]
140
+ _dir()
141
+ fd = os.open(_sessions_path(), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
142
+ with os.fdopen(fd, "w") as f:
143
+ f.write(json.dumps(data, ensure_ascii=False))
144
+ return True
145
+
146
+
129
147
  # --- Named-agent registry ------------------------------------------------------
130
148
  # Maps a human-friendly agent name (the a2a agent-card name, an openclaw claw
131
149
  # Name) → a non-secret connection snapshot, so `arkclaw chat <name>` works.
@@ -83,6 +83,13 @@ class StsError(AuthError):
83
83
  code = "ARKCLAW_E_STS"
84
84
 
85
85
 
86
+ class ClawAccessDeniedError(StsError):
87
+ """The user is not authorized for this specific claw (server per-user gate).
88
+ Distinct so callers can prune it from local history (self-healing)."""
89
+
90
+ code = "ARKCLAW_E_FORBIDDEN"
91
+
92
+
86
93
  # --- Secret redaction -------------------------------------------------------
87
94
  # Never let a full token reach the terminal, a log, or an error message.
88
95
  #
@@ -11,6 +11,7 @@ and every error is a typed :class:`~ee_claw.errors.ArkclawError`.
11
11
  from __future__ import annotations
12
12
 
13
13
  import asyncio
14
+ import dataclasses
14
15
  import json
15
16
  import os
16
17
  import pathlib
@@ -29,6 +30,7 @@ from ee_claw.attachments import collect_files
29
30
  from ee_claw.config import SessionConfig
30
31
  from ee_claw.errors import (
31
32
  ArkclawError,
33
+ ClawAccessDeniedError,
32
34
  ExpiredError,
33
35
  NetworkError,
34
36
  NoClawError,
@@ -706,6 +708,15 @@ def _save_session_state(cfg: SessionConfig, target: str, session: str | None, tr
706
708
  config_mod.record_session(str(cfg.space), target, session or "main", extra)
707
709
 
708
710
 
711
+ def _forget_inaccessible(cfg: SessionConfig, claw: str) -> None:
712
+ """A chat to ``claw`` was denied (not the user's, not shared). Drop it from
713
+ local history so `arkclaw agents` stops listing it, and clear it as the
714
+ default if it was the default (so a bare `arkclaw chat` won't keep failing)."""
715
+ config_mod.forget_claw(str(cfg.space), claw)
716
+ if cfg.claw == claw:
717
+ config_mod.save_config(dataclasses.replace(cfg, claw=None))
718
+
719
+
709
720
  def _resolve_target(name: str) -> SessionConfig | None:
710
721
  """A chat target by name: the agent registry (card/claw names recorded by
711
722
  ``login``/``agents``) first, then profile names."""
@@ -782,13 +793,18 @@ def do_chat(
782
793
  target, transport = _target_transport(
783
794
  cfg, clawid, id_token, session=session, approver=approver
784
795
  )
785
- config_mod.record_session(str(cfg.space), target, session or "main")
786
-
796
+ # NOTE: do NOT record the session here (before the turn) a denied claw
797
+ # would pollute `arkclaw agents`. Recording happens only after a turn
798
+ # SUCCEEDS, via _save_session_state below.
787
799
  if message is not None:
788
800
  stdin_text = _stdin_context()
789
801
  if stdin_text:
790
802
  attachments.insert(0, Attachment(name="<stdin>", content=stdin_text.encode()))
791
- result = asyncio.run(_run_turn(transport, message, emitter, files=attachments))
803
+ try:
804
+ result = asyncio.run(_run_turn(transport, message, emitter, files=attachments))
805
+ except ClawAccessDeniedError:
806
+ _forget_inaccessible(cfg, target) # self-heal: drop it from `agents`
807
+ raise
792
808
  emitter.line("")
793
809
  if result.timed_out:
794
810
  emitter.line("⚠ 回合超时结束,回复可能不完整。")
@@ -832,6 +848,8 @@ def do_chat(
832
848
  except ArkclawError as e:
833
849
  # One bad turn (server failure, network blip) must not kill the
834
850
  # REPL — report it and keep the conversation open.
851
+ if isinstance(e, ClawAccessDeniedError):
852
+ _forget_inaccessible(cfg, target) # self-heal: drop it from `agents`
835
853
  emitter.line(f"\n✗ {e.code}: {e.message}")
836
854
  if e.hint:
837
855
  emitter.line(f" {e.hint}")
@@ -16,7 +16,7 @@ import urllib.error
16
16
  import urllib.parse
17
17
  import urllib.request
18
18
 
19
- from ee_claw.errors import NetworkError, StsError, redact
19
+ from ee_claw.errors import ClawAccessDeniedError, NetworkError, StsError, redact
20
20
 
21
21
  STS_HOST = "sts.volcengineapi.com"
22
22
  STS_VERSION = "2018-01-01"
@@ -164,7 +164,7 @@ def get_chat_token(
164
164
  err_body = e.read().decode(errors="replace")
165
165
  code, message = _volc_error(err_body)
166
166
  if code == "AccessDenied" or "AccessDenied" in err_body: # coruscant per-user gate
167
- raise StsError(
167
+ raise ClawAccessDeniedError(
168
168
  "权限不足:当前用户无权访问该 Claw 实例(非所有者且未获授权)。",
169
169
  hint="运行 `arkclaw agents` 查看你有权访问的 Claw 实例,或使用 `--clawid` 指定其它实例。",
170
170
  ) from e
@@ -1,6 +1,10 @@
1
- """OpenClaw WebSocket transport (protocol v3).
1
+ """OpenClaw WebSocket transport (protocol v3–v4, negotiated).
2
2
 
3
3
  Refactored from the live-validated ``core.ws_chat`` (2026-06-05, real EE claw).
4
+ The connect frame negotiates ``minProtocol=3 .. maxProtocol=4`` so it works
5
+ against both older claws (v3) and OpenClaw ≥5.28 (v4); the server picks the
6
+ version. (The post-connect frame shapes below were calibrated on a live v3
7
+ claw; v4 is expected to be compatible for these — verify against a v4 claw.)
4
8
  Confirmed protocol details, preserved exactly:
5
9
 
6
10
  * The ChatToken from ``GetClawInstanceChatToken`` is **single-use** — a fresh
@@ -9,7 +13,7 @@ Confirmed protocol details, preserved exactly:
9
13
  ``connect.challenge`` event (no response required), then expects a
10
14
  ``connect`` request whose ``client.id`` MUST be ``openclaw-control-ui``
11
15
  (server whitelist; anything else → INVALID_REQUEST), with
12
- ``role=operator`` / ``scopes=["operator.admin"]`` / protocol pinned to 3.
16
+ ``role=operator`` / ``scopes=["operator.admin"]`` / protocol negotiated v3–v4.
13
17
  * ``chat.send`` (sessionKey ``agent:main:main``) → ``{ok:true, payload:{runId,
14
18
  status:"started"}}``, then ``agent`` events stream assistant text
15
19
  (``data.delta`` incremental) and the ``chat`` event with ``state=final``
@@ -31,6 +35,8 @@ from __future__ import annotations
31
35
 
32
36
  import asyncio
33
37
  import json
38
+ import os
39
+ import sys
34
40
  import urllib.parse
35
41
  import uuid
36
42
  from collections.abc import Sequence
@@ -43,6 +49,11 @@ from ee_claw.transport.base import Approver, Attachment, OnEvent, TurnEvent, Tur
43
49
 
44
50
  SESSION_KEY = "agent:main:main" # OpenClaw default session
45
51
  WS_CLIENT_ID = "openclaw-control-ui" # ONLY this client.id is accepted
52
+ # ws protocol versions this client speaks; the server negotiates the highest
53
+ # mutually-supported one (v3 for older claws, v4 for OpenClaw ≥ 5.28). Pinning a
54
+ # single version makes a newer claw reject the connect with PROTOCOL_MISMATCH.
55
+ MIN_PROTOCOL = 3
56
+ MAX_PROTOCOL = 4
46
57
 
47
58
 
48
59
  def session_key_for(name: str | None) -> str:
@@ -61,14 +72,40 @@ class _Ws(Protocol):
61
72
  async def recv(self) -> str | bytes: ...
62
73
 
63
74
 
75
+ class _DebugWs:
76
+ """Frame tracer. With ``ARKCLAW_DEBUG_WS`` set, dump every ws frame (sent
77
+ and received) to stderr — redacted (no tokens) and length-capped — for
78
+ diagnosing protocol changes (e.g. OpenClaw v4) against a live claw."""
79
+
80
+ def __init__(self, ws: Any) -> None:
81
+ self._ws = ws
82
+
83
+ async def send(self, data: str) -> None:
84
+ sys.stderr.write(f"[ws →] {redact(data)[:2000]}\n")
85
+ sys.stderr.flush()
86
+ await self._ws.send(data)
87
+
88
+ async def recv(self) -> str | bytes:
89
+ data = await self._ws.recv()
90
+ text = data if isinstance(data, str) else bytes(data).decode("utf-8", "replace")
91
+ sys.stderr.write(f"[ws ←] {redact(text)[:2000]}\n")
92
+ sys.stderr.flush()
93
+ return data
94
+
95
+
96
+ def _wrap_debug(ws: Any) -> Any:
97
+ """Wrap ``ws`` in a frame tracer when ARKCLAW_DEBUG_WS is set; else pass through."""
98
+ return _DebugWs(ws) if os.environ.get("ARKCLAW_DEBUG_WS") else ws
99
+
100
+
64
101
  def connect_frame() -> dict[str, Any]:
65
102
  return {
66
103
  "type": "req",
67
104
  "id": str(uuid.uuid4()),
68
105
  "method": "connect",
69
106
  "params": {
70
- "minProtocol": 3,
71
- "maxProtocol": 3,
107
+ "minProtocol": MIN_PROTOCOL,
108
+ "maxProtocol": MAX_PROTOCOL,
72
109
  "client": {
73
110
  "id": WS_CLIENT_ID,
74
111
  "version": "arkclaw-cli",
@@ -369,6 +406,7 @@ class OpenClawTransport:
369
406
  connect = websockets.connect
370
407
  try:
371
408
  async with connect(url, max_size=None) as ws:
409
+ ws = _wrap_debug(ws)
372
410
  self._active_ws = ws
373
411
  try:
374
412
  return await drive_chat(
@@ -428,6 +466,7 @@ class OpenClawTransport:
428
466
  connect = websockets.connect
429
467
  try:
430
468
  async with connect(url, max_size=None) as ws:
469
+ ws = _wrap_debug(ws)
431
470
  await ws.send(json.dumps(connect_frame()))
432
471
  loop = asyncio.get_running_loop()
433
472
  deadline = loop.time() + self.turn_timeout