arkclaw-webchat-cli 0.5.3__tar.gz → 0.6.0__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.3 → arkclaw_webchat_cli-0.6.0}/PKG-INFO +1 -1
  2. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/pyproject.toml +1 -1
  3. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/flows.py +114 -46
  4. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/identity.py +14 -0
  5. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/.gitignore +0 -0
  6. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/README.md +0 -0
  7. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/__init__.py +0 -0
  8. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/attachments.py +0 -0
  9. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/cli.py +0 -0
  10. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/config.py +0 -0
  11. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/control.py +0 -0
  12. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/core.py +0 -0
  13. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/doctor.py +0 -0
  14. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/errors.py +0 -0
  15. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/oauth.py +0 -0
  16. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/output.py +0 -0
  17. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/policy.py +0 -0
  18. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/providers.py +0 -0
  19. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/secrets_store.py +0 -0
  20. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/sts.py +0 -0
  21. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/transport/__init__.py +0 -0
  22. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/transport/a2a.py +0 -0
  23. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/transport/base.py +0 -0
  24. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/src/ee_claw/transport/openclaw.py +0 -0
  25. {arkclaw_webchat_cli-0.5.3 → arkclaw_webchat_cli-0.6.0}/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.3
3
+ Version: 0.6.0
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.3"
7
+ version = "0.6.0"
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"
@@ -53,7 +53,7 @@ from ee_claw.identity import (
53
53
  from ee_claw.oauth import OAuthClient
54
54
  from ee_claw.output import Emitter
55
55
  from ee_claw.providers import ProviderContext, resolve_token_source
56
- from ee_claw.sts import assume_role_with_oidc
56
+ from ee_claw.sts import assume_role_with_oidc, get_chat_token
57
57
  from ee_claw.transport.a2a import A2ATransport, agent_card
58
58
  from ee_claw.transport.base import Approver, Attachment, Transport, TurnEvent, TurnResult
59
59
  from ee_claw.transport.openclaw import OpenClawTransport, session_key_for
@@ -116,6 +116,37 @@ def _user_identity(id_token: str | None) -> dict[str, str]:
116
116
  return ident
117
117
 
118
118
 
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)."""
127
+ from concurrent.futures import ThreadPoolExecutor
128
+
129
+ creds = _openclaw_creds(cfg, id_token) # may raise (offline / expired / no creds)
130
+ identity = _user_identity(id_token)
131
+ region = str(cfg.region)
132
+
133
+ def check(cid: str) -> tuple[str, bool]:
134
+ try:
135
+ get_chat_token(cid, region, creds, identity=identity)
136
+ return cid, True
137
+ except ClawAccessDeniedError:
138
+ return cid, False
139
+ except ArkclawError:
140
+ return cid, True # transient/other — keep, don't silently drop
141
+
142
+ accessible: set[str] = set()
143
+ denied: set[str] = set()
144
+ 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
148
+
149
+
119
150
  def _normalize(url: str) -> tuple[str, str]:
120
151
  base = (url if "//" in url else "https://" + url).rstrip("/")
121
152
  host = urllib.parse.urlparse(base).hostname or ""
@@ -131,55 +162,72 @@ def _is_userpool_address(host: str) -> bool:
131
162
 
132
163
 
133
164
  def do_init(emitter: Emitter, address: str | None = None) -> dict[str, Any]:
134
- """``arkclaw init``: one-time interactive setup. Captures the login address
135
- plus the bits not yet auto-discoverable (the CLI's OAuth client, the STS
136
- role) so that afterwards ``arkclaw login`` needs no env vars or flags.
137
- Anything the space already self-describes (issuer, region, and once the
138
- platform publishes it client_id/role) is auto-filled and not asked."""
139
- if emitter.json or not sys.stdin.isatty():
140
- raise ValidationError(
141
- "arkclaw init 是交互式的,需要终端。",
142
- hint="在终端直接运行 `arkclaw init`;脚本里改用 env(ARKCLAW_CLIENT_ID/ROLE_TRN)+ flags。",
143
- )
165
+ """``arkclaw init``: one-time setup. The GOAL is **address-only** — when the
166
+ space publishes its CLI config (issuer/region/client_id/role…) via
167
+ ``/.well-known/arkclaw-cli`` or GetAuthConfig, the user gives ONLY the
168
+ address and nothing is asked (works non-interactively too). Until then, it
169
+ prompts for the few bits discovery can't provide (the CLI's OAuth client,
170
+ the STS role)."""
171
+ interactive = not emitter.json and sys.stdin.isatty()
144
172
 
145
- def ask(label: str, default: str | None = None, *, required: bool = False) -> str | None:
173
+ def need_tty() -> None:
174
+ if not interactive:
175
+ raise ValidationError(
176
+ "该空间尚未发布 CLI 配置,需要补充 client_id/role,而当前非交互终端。",
177
+ hint="在终端运行 `arkclaw init <地址>`;或脚本里用 env(ARKCLAW_CLIENT_ID/ROLE_TRN)+ flags。",
178
+ )
179
+
180
+ def ask(label: str, default: str | None = None, *, required: bool = False) -> str:
146
181
  suffix = f" [{default}]" if default else ""
147
182
  while True:
148
183
  ans = input(f" {label}{suffix}: ").strip() or (default or "")
149
184
  if ans or not required:
150
- return ans or None
185
+ return ans
151
186
  emitter.line(" (必填)")
152
187
 
153
- emitter.line("arkclaw init —— 一次性配置(能自动发现的不会问你)\n")
154
- address = address or ask("空间登录地址 (https://…)", required=True)
188
+ if not address:
189
+ need_tty()
190
+ emitter.line("arkclaw init —— 配置一次(能自动发现的不问你)\n")
191
+ address = ask("空间登录地址 (https://…)", required=True)
155
192
  base, host = _normalize(str(address))
156
193
  disc = discover_cli_config(base) or {}
157
194
  issuer = disc.get("issuer") or (f"https://{host}" if _is_userpool_address(host) else None)
158
195
  region = disc.get("region") or derive_region(base)
159
- if issuer:
160
- emitter.line(" ✓ 自动发现身份池 issuer")
161
- if region:
162
- emitter.line(f" ✓ 区域: {region}")
163
-
164
- # User-facing name is "webchat" (the ChatToken/wss path); internally it's
165
- # the "openclaw" transport. "a2a" is unchanged.
166
- disc_t = str(disc.get("transport") or "")
167
- transport_in = ask("传输方式 (webchat/a2a)", default=("a2a" if disc_t == "a2a" else "webchat")) or "webchat"
168
- transport = "a2a" if transport_in.lower() == "a2a" else "openclaw"
169
- client_id = disc.get("client_id") or ask(
170
- "CLI client_id(public OAuth 客户端,问管理员;平台发布发现后免此项)", required=True
171
- )
172
- role_trn: object = None
173
- endpoint: object = None
174
- if transport == "openclaw":
175
- role_trn = disc.get("role_trn") or ask("STS role TRN (trn:iam::<账号>:role/<名字>)", required=True)
196
+ transport = "a2a" if str(disc.get("transport") or "").lower() == "a2a" else "openclaw"
197
+ client_id = disc.get("client_id")
198
+ role_trn = disc.get("role_trn")
199
+ provider_trn = disc.get("provider_trn")
200
+ endpoint = disc.get("endpoint")
201
+ clawid = disc.get("clawid")
202
+
203
+ # Fully discovered = the space published this CLI's client + target plane →
204
+ # nothing to ask (address-only). Otherwise prompt ONLY for what's missing.
205
+ complete = bool(client_id) and (bool(role_trn) if transport == "openclaw" else bool(endpoint))
206
+ if complete:
207
+ emitter.line("✓ 该空间已发布 CLI 配置 —— 全部自动解析,无需手动输入。")
176
208
  else:
177
- endpoint = disc.get("endpoint") or ask("A2A endpoint (https://…)", required=True)
178
- clawid = disc.get("clawid") or ask("默认 claw id (ci-…,可留空,登录后用 arkclaw agents 选)")
209
+ need_tty()
210
+ if issuer:
211
+ emitter.line(" ✓ 自动发现身份池 issuer")
212
+ if region:
213
+ emitter.line(f" ✓ 区域: {region}")
214
+ # "webchat" = the ChatToken/wss path (internal name "openclaw"); "a2a" unchanged.
215
+ t_in = ask("传输方式 (webchat/a2a)", default=("a2a" if transport == "a2a" else "webchat"))
216
+ transport = "a2a" if t_in.lower() == "a2a" else "openclaw"
217
+ if not client_id:
218
+ client_id = ask(
219
+ "CLI client_id(public OAuth 客户端,问管理员;平台发布发现后免此项)", required=True
220
+ )
221
+ if transport == "openclaw" and not role_trn:
222
+ role_trn = ask("STS role TRN (trn:iam::<账号>:role/<名字>)", required=True)
223
+ elif transport == "a2a" and not endpoint:
224
+ endpoint = ask("A2A endpoint (https://…)", required=True)
225
+ if not clawid:
226
+ clawid = ask("默认 claw id (ci-…,可留空,登录后用 arkclaw agents 选)") or None
179
227
 
180
228
  saved = {
181
229
  "address": base, "issuer": issuer, "client_id": client_id, "transport": transport,
182
- "region": region, "role_trn": role_trn, "provider_trn": disc.get("provider_trn"),
230
+ "region": region, "role_trn": role_trn, "provider_trn": provider_trn,
183
231
  "endpoint": endpoint, "space_id": disc.get("space_id"), "clawid": clawid,
184
232
  }
185
233
  config_mod.save_defaults(saved)
@@ -982,12 +1030,11 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
982
1030
  )
983
1031
  elif cfg.transport == "openclaw":
984
1032
  emitter.line(f"★ 当前登录(openclaw)· 空间 {cfg.space}")
985
- # The claws you can chat = the ones YOU have used from this machine,
986
- # remembered locally — NOT a server enumeration of the space. The
987
- # space-wide ListClawInstances returns every member's claws to anyone
988
- # (no per-user scoping reachable by the CLI's role), so we don't call it.
989
- # New claws enter via `arkclaw chat ci-...`; each chat is recorded
990
- # (record_session) and shows up here next time.
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).
991
1038
  seen: dict[str, float] = {}
992
1039
  for s in config_mod.list_sessions():
993
1040
  cid = s.get("claw")
@@ -996,12 +1043,29 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
996
1043
  seen[str(cid)] = max(seen.get(str(cid), 0.0), float(ts) if isinstance(ts, (int, float)) else 0.0)
997
1044
  if cfg.claw and str(cfg.claw) not in seen:
998
1045
  seen[str(cfg.claw)] = 0.0 # the default claw, even if not chatted yet
999
- claws = [
1000
- {"ClawInstanceId": cid, "last_used": ts}
1001
- for cid, ts in sorted(seen.items(), key=lambda kv: kv[1], reverse=True)
1002
- ]
1046
+
1047
+ claw_ids = sorted(seen, key=lambda c: seen[c], reverse=True)
1048
+ checked = False
1049
+ if claw_ids:
1050
+ if not emitter.json:
1051
+ emitter.line(" 核对访问权限中…")
1052
+ try:
1053
+ _, id_token = _load_session(cfg)
1054
+ accessible, denied = _probe_claw_access(cfg, id_token, claw_ids)
1055
+ 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]
1063
+ except ArkclawError:
1064
+ checked = False # offline / expired / no creds → show unchecked
1065
+
1066
+ claws = [{"ClawInstanceId": cid, "last_used": seen[cid]} for cid in claw_ids]
1003
1067
  current["claws"] = claws
1004
- current["source"] = "local-history"
1068
+ current["source"] = "verified" if checked else "local-history-unchecked"
1005
1069
  if claws:
1006
1070
  emitter.table(
1007
1071
  ["Claw(可 chat)", "上次使用", "默认"],
@@ -1014,6 +1078,10 @@ def do_agents(emitter: Emitter) -> dict[str, Any]:
1014
1078
  for c in claws
1015
1079
  ],
1016
1080
  )
1081
+ if not checked:
1082
+ emitter.line(" ⚠ 未能核对权限(离线/会话过期),以上为本地历史,可能含你无权访问的。")
1083
+ elif checked:
1084
+ emitter.line(" 你在本空间没有可访问的 claw(历史里的都已无权或不存在)。")
1017
1085
  else:
1018
1086
  emitter.line(" 还没用过任何 claw。用 `arkclaw chat ci-xxxx` 开始,之后这里会记住它。")
1019
1087
 
@@ -118,6 +118,20 @@ def discover_cli_config(url: str) -> dict[str, object] | None:
118
118
  if result.get(k):
119
119
  out["client_id"] = result[k]
120
120
  break
121
+ # The remaining CLI-plane fields (platform-added). Once the BFF surfaces
122
+ # these on GetAuthConfig, the user types ONLY the address — login/init
123
+ # resolve everything. (camelCase from the BFF → snake_case the CLI uses.)
124
+ for src, dst in (
125
+ ("roleTrn", "role_trn"),
126
+ ("providerTrn", "provider_trn"),
127
+ ("region", "region"),
128
+ ("spaceId", "space_id"),
129
+ ("transport", "transport"),
130
+ ("endpoint", "endpoint"),
131
+ ("defaultClawId", "clawid"),
132
+ ):
133
+ if result.get(src):
134
+ out[dst] = result[src]
121
135
  return out or None
122
136
 
123
137