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.
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/PKG-INFO +1 -1
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/pyproject.toml +1 -1
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/config.py +18 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/errors.py +7 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/flows.py +21 -3
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/sts.py +2 -2
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/openclaw.py +43 -4
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/.gitignore +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/README.md +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/__init__.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/attachments.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/cli.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/control.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/core.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/doctor.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/identity.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/oauth.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/output.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/policy.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/providers.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/secrets_store.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/__init__.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/a2a.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/transport/base.py +0 -0
- {arkclaw_webchat_cli-0.5.1 → arkclaw_webchat_cli-0.5.3}/src/ee_claw/update.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arkclaw-webchat-cli"
|
|
7
|
-
version = "0.5.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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":
|
|
71
|
-
"maxProtocol":
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|