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.
Files changed (25) hide show
  1. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/PKG-INFO +1 -1
  2. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/pyproject.toml +1 -1
  3. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/__init__.py +8 -1
  4. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/cli.py +15 -0
  5. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/flows.py +33 -2
  6. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/sts.py +42 -5
  7. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/openclaw.py +3 -2
  8. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/.gitignore +0 -0
  9. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/README.md +0 -0
  10. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/attachments.py +0 -0
  11. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/config.py +0 -0
  12. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/control.py +0 -0
  13. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/core.py +0 -0
  14. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/doctor.py +0 -0
  15. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/errors.py +0 -0
  16. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/identity.py +0 -0
  17. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/oauth.py +0 -0
  18. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/output.py +0 -0
  19. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/policy.py +0 -0
  20. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/providers.py +0 -0
  21. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/secrets_store.py +0 -0
  22. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/__init__.py +0 -0
  23. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/a2a.py +0 -0
  24. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/src/ee_claw/transport/base.py +0 -0
  25. {arkclaw_webchat_cli-0.4.0 → arkclaw_webchat_cli-0.5.1}/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.4.0
3
+ Version: 0.5.1
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.4.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
- __version__ = "0.1.0"
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(cfg, clawid, creds, session=session, approver=approver)
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(clawid=clawid, region=str(cfg.region), creds=creds), clawid
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 get_chat_token(clawid: str, region: str, creds: dict[str, str]) -> tuple[str, str]:
125
- """GetClawInstanceChatToken with temp creds (ChatToken, Endpoint)."""
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
- body = json.dumps({"ClawInstanceId": clawid, "ProjectName": "default"}).encode()
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
- # Redact BEFORE truncating so a split token can't slip past the regex.
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"GetClawInstanceChatToken failed: {redact(e.read().decode(errors='replace'))[:300]}"
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)}"