arkclaw-webchat-cli 0.4.0__tar.gz → 0.5.1__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.4.0 → arkclaw_webchat_cli-0.5.1}/PKG-INFO +1 -1
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/pyproject.toml +1 -1
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/__init__.py +8 -1
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/cli.py +15 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/flows.py +33 -2
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/sts.py +42 -5
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/openclaw.py +3 -2
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/.gitignore +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/README.md +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/attachments.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/config.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/control.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/core.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/doctor.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/errors.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/identity.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/oauth.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/output.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/policy.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/providers.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/secrets_store.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/__init__.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/a2a.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/base.py +0 -0
- {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/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.
|
|
7
|
+
version = "0.5.1"
|
|
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"
|
|
@@ -10,6 +10,13 @@ AssumeRoleWithOIDC → GetClawInstanceChatToken → OpenClaw WebSocket).
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
from importlib.metadata import PackageNotFoundError
|
|
14
|
+
from importlib.metadata import version as _pkg_version
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# Single source of truth: the installed package metadata (pyproject version).
|
|
18
|
+
__version__ = _pkg_version("arkclaw-webchat-cli")
|
|
19
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
20
|
+
__version__ = "0.0.0+local"
|
|
14
21
|
|
|
15
22
|
__all__ = ["__version__"]
|
|
@@ -436,7 +436,22 @@ def _rewrite_agent_shortcut(argv: list[str]) -> None:
|
|
|
436
436
|
argv.insert(1, "chat")
|
|
437
437
|
|
|
438
438
|
|
|
439
|
+
@app.command()
|
|
440
|
+
def version() -> None:
|
|
441
|
+
"""显示已安装的 arkclaw 版本。"""
|
|
442
|
+
from ee_claw import __version__
|
|
443
|
+
|
|
444
|
+
typer.echo(f"arkclaw {__version__}")
|
|
445
|
+
|
|
446
|
+
|
|
439
447
|
def main() -> None:
|
|
448
|
+
# `--version` / `-V` is handled here (before Typer) so it works as a bare
|
|
449
|
+
# top-level flag without a group callback. `arkclaw version` is the command.
|
|
450
|
+
if len(sys.argv) >= 2 and sys.argv[1] in ("--version", "-V"):
|
|
451
|
+
from ee_claw import __version__
|
|
452
|
+
|
|
453
|
+
print(f"arkclaw {__version__}")
|
|
454
|
+
return
|
|
440
455
|
_rewrite_agent_shortcut(sys.argv)
|
|
441
456
|
app()
|
|
442
457
|
|
|
@@ -90,6 +90,30 @@ def _openclaw_creds(cfg: SessionConfig, id_token: str | None) -> dict[str, str]:
|
|
|
90
90
|
return assume_role_with_oidc(str(id_token), str(cfg.role_trn), cfg.provider_trn)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def _user_identity(id_token: str | None) -> dict[str, str]:
|
|
94
|
+
"""The SSO user's identity from the *verified* id_token, for per-user authz
|
|
95
|
+
at GetClawInstanceChatToken. When the CLI's STS role is a space *resource*
|
|
96
|
+
account, the control plane checks these against the claw's owner and denies
|
|
97
|
+
a non-owner (fail-closed) — so a leaked claw_id can't chat someone else's
|
|
98
|
+
claw. Read from the token ONLY (never user input), so it can't impersonate."""
|
|
99
|
+
if not id_token:
|
|
100
|
+
return {}
|
|
101
|
+
try:
|
|
102
|
+
claims = decode_claims(id_token)
|
|
103
|
+
except ArkclawError:
|
|
104
|
+
return {}
|
|
105
|
+
ident: dict[str, str] = {}
|
|
106
|
+
if claims.get("sub"):
|
|
107
|
+
ident["UserPoolUserUid"] = str(claims["sub"])
|
|
108
|
+
if claims.get("email"):
|
|
109
|
+
ident["Email"] = str(claims["email"])
|
|
110
|
+
for c in ("union_id", "uid"):
|
|
111
|
+
if claims.get(c):
|
|
112
|
+
ident["UnionId"] = str(claims[c])
|
|
113
|
+
break
|
|
114
|
+
return ident
|
|
115
|
+
|
|
116
|
+
|
|
93
117
|
def _normalize(url: str) -> tuple[str, str]:
|
|
94
118
|
base = (url if "//" in url else "https://" + url).rstrip("/")
|
|
95
119
|
host = urllib.parse.urlparse(base).hostname or ""
|
|
@@ -472,6 +496,7 @@ def make_transport(
|
|
|
472
496
|
*,
|
|
473
497
|
session: str | None = None,
|
|
474
498
|
approver: Approver | None = None,
|
|
499
|
+
identity: dict[str, str] | None = None,
|
|
475
500
|
) -> Transport:
|
|
476
501
|
"""The target-plane seam: pick the adapter from config, not code. The
|
|
477
502
|
abstract session name is mapped to transport-specific addressing inside
|
|
@@ -483,6 +508,7 @@ def make_transport(
|
|
|
483
508
|
clawid=clawid,
|
|
484
509
|
region=cfg.region,
|
|
485
510
|
creds=creds,
|
|
511
|
+
identity=identity,
|
|
486
512
|
session_key=session_key_for(session),
|
|
487
513
|
approver=approver,
|
|
488
514
|
)
|
|
@@ -667,7 +693,10 @@ def _target_transport(
|
|
|
667
693
|
hint="请加 --clawid <ci-...>(或 login 时带 --clawid 设默认)。",
|
|
668
694
|
)
|
|
669
695
|
creds = _openclaw_creds(cfg, id_token)
|
|
670
|
-
return clawid, make_transport(
|
|
696
|
+
return clawid, make_transport(
|
|
697
|
+
cfg, clawid, creds, session=session, approver=approver,
|
|
698
|
+
identity=_user_identity(id_token),
|
|
699
|
+
)
|
|
671
700
|
|
|
672
701
|
|
|
673
702
|
def _save_session_state(cfg: SessionConfig, target: str, session: str | None, transport: Transport) -> None:
|
|
@@ -991,7 +1020,9 @@ def _openclaw_file_transport(clawid: str | None) -> tuple[OpenClawTransport, str
|
|
|
991
1020
|
if not cfg.region:
|
|
992
1021
|
raise RegionError("登录信息缺少区域。", hint="重新 login 并加 --region。")
|
|
993
1022
|
creds = _openclaw_creds(cfg, id_token)
|
|
994
|
-
return OpenClawTransport(
|
|
1023
|
+
return OpenClawTransport(
|
|
1024
|
+
clawid=clawid, region=str(cfg.region), creds=creds, identity=_user_identity(id_token)
|
|
1025
|
+
), clawid
|
|
995
1026
|
|
|
996
1027
|
|
|
997
1028
|
def do_ls(emitter: Emitter, *, clawid: str | None = None) -> dict[str, Any]:
|
|
@@ -121,11 +121,34 @@ def assume_role_with_oidc(
|
|
|
121
121
|
return creds
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
124
|
+
def _volc_error(body: str) -> tuple[str, str]:
|
|
125
|
+
"""Extract ``(Code, Message)`` from a Volcengine error envelope
|
|
126
|
+
(``ResponseMetadata.Error``); ``('', '')`` if it can't be parsed. Lets the
|
|
127
|
+
CLI surface a clean message instead of dumping the raw JSON envelope."""
|
|
128
|
+
try:
|
|
129
|
+
err = json.loads(body)["ResponseMetadata"]["Error"]
|
|
130
|
+
return str(err.get("Code") or ""), str(err.get("Message") or "")
|
|
131
|
+
except (ValueError, KeyError, TypeError):
|
|
132
|
+
return "", ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_chat_token(
|
|
136
|
+
clawid: str, region: str, creds: dict[str, str], *, identity: dict[str, str] | None = None
|
|
137
|
+
) -> tuple[str, str]:
|
|
138
|
+
"""GetClawInstanceChatToken with temp creds → (ChatToken, Endpoint).
|
|
139
|
+
|
|
140
|
+
``identity`` carries the SSO user's identity (``UserPoolUserUid``/``Email``/
|
|
141
|
+
``UnionId``) taken from the *verified* id_token. The control plane's
|
|
142
|
+
resource-account branch authorizes the user against the claw's owner with
|
|
143
|
+
these fields and denies a non-owner (fail-closed) — so a leaked claw_id is
|
|
144
|
+
not enough to chat someone else's claw. (Sent always; the owning-tenant
|
|
145
|
+
branch ignores it.)"""
|
|
126
146
|
host = f"arkclaw.{region}.volcengineapi.com"
|
|
127
147
|
query = {"Action": "GetClawInstanceChatToken", "Version": ARKCLAW_API_VERSION}
|
|
128
|
-
|
|
148
|
+
payload: dict[str, str] = {"ClawInstanceId": clawid, "ProjectName": "default"}
|
|
149
|
+
if identity:
|
|
150
|
+
payload.update({k: v for k, v in identity.items() if v})
|
|
151
|
+
body = json.dumps(payload).encode()
|
|
129
152
|
headers = sign_headers(
|
|
130
153
|
"POST", host, query, body,
|
|
131
154
|
ak=creds["AccessKeyId"], sk=creds["SecretAccessKey"],
|
|
@@ -138,9 +161,23 @@ def get_chat_token(clawid: str, region: str, creds: dict[str, str]) -> tuple[str
|
|
|
138
161
|
urllib.request.Request(url, data=body, headers=headers), timeout=30
|
|
139
162
|
).read()
|
|
140
163
|
except urllib.error.HTTPError as e:
|
|
141
|
-
|
|
164
|
+
err_body = e.read().decode(errors="replace")
|
|
165
|
+
code, message = _volc_error(err_body)
|
|
166
|
+
if code == "AccessDenied" or "AccessDenied" in err_body: # coruscant per-user gate
|
|
167
|
+
raise StsError(
|
|
168
|
+
"权限不足:当前用户无权访问该 Claw 实例(非所有者且未获授权)。",
|
|
169
|
+
hint="运行 `arkclaw agents` 查看你有权访问的 Claw 实例,或使用 `--clawid` 指定其它实例。",
|
|
170
|
+
) from e
|
|
171
|
+
if code == "ErrMissingUserIdentity": # gate ran but the CLI sent no identity
|
|
172
|
+
raise StsError(
|
|
173
|
+
"权限校验失败:请求未包含用户身份信息。",
|
|
174
|
+
hint="请将 CLI 升级至最新版本(uv tool upgrade arkclaw-webchat-cli),并重新登录(arkclaw login)。",
|
|
175
|
+
) from e
|
|
176
|
+
# Anything else: surface the clean Code/Message — never the raw JSON envelope.
|
|
177
|
+
# Redact first so a split token can't slip past the regex.
|
|
178
|
+
clean = redact(message or err_body)[:200]
|
|
142
179
|
raise StsError(
|
|
143
|
-
f"
|
|
180
|
+
f"获取会话令牌失败:{clean}", hint=(f"服务端错误码:{code}" if code else None)
|
|
144
181
|
) from e
|
|
145
182
|
except urllib.error.URLError as e:
|
|
146
183
|
raise NetworkError(f"cannot reach {host}: {e.reason}") from e
|
|
@@ -334,6 +334,7 @@ class OpenClawTransport:
|
|
|
334
334
|
clawid: str
|
|
335
335
|
region: str
|
|
336
336
|
creds: dict[str, str]
|
|
337
|
+
identity: dict[str, str] | None = None # SSO user identity for per-claw authz
|
|
337
338
|
session_key: str = SESSION_KEY
|
|
338
339
|
approver: Approver | None = None
|
|
339
340
|
idle_timeout: float = 30.0
|
|
@@ -355,7 +356,7 @@ class OpenClawTransport:
|
|
|
355
356
|
TurnEvent(kind="info", text=f"⚠ 二进制文件无法内联,已跳过: {name}")
|
|
356
357
|
)
|
|
357
358
|
on_event(TurnEvent(kind="status", text="获取 ChatToken"))
|
|
358
|
-
chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds)
|
|
359
|
+
chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds, identity=self.identity)
|
|
359
360
|
on_event(TurnEvent(kind="status", text="连接 claw(wss)"))
|
|
360
361
|
url = (
|
|
361
362
|
f"wss://{endpoint}/?chatToken={urllib.parse.quote(chat_token)}"
|
|
@@ -415,7 +416,7 @@ class OpenClawTransport:
|
|
|
415
416
|
async def _request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
416
417
|
"""connect (hello-ok) → one control request → its res payload. Errors
|
|
417
418
|
are redacted; the token-bearing URL is never echoed."""
|
|
418
|
-
chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds)
|
|
419
|
+
chat_token, endpoint = get_chat_token(self.clawid, self.region, self.creds, identity=self.identity)
|
|
419
420
|
url = (
|
|
420
421
|
f"wss://{endpoint}/?chatToken={urllib.parse.quote(chat_token)}"
|
|
421
422
|
f"&clawInstanceId={urllib.parse.quote(self.clawid)}"
|
|
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
|