agentkit-sdk-python 0.6.5__py3-none-any.whl → 0.7.1__py3-none-any.whl

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.
@@ -0,0 +1,360 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Bring a user's *own* third-party model subscription into the sandbox.
16
+
17
+ Many foreign models (ChatGPT/Codex, Claude Code) are used via a JWT obtained by an
18
+ interactive SSO login, not a static API key. A user who subscribed to one of those
19
+ plans wants the **sandbox's** codex to run on *their* subscription.
20
+
21
+ The agentkit UserPool does not (and need not) federate with OpenAI/Anthropic. Codex
22
+ already supports SSO **locally**; in the sandbox it is "remote", so the agreed design
23
+ is dead simple and is what this module implements:
24
+
25
+ 1. The user runs the provider's native SSO **on their PC** (``codex login`` for
26
+ Codex/ChatGPT, ``claude`` for Claude Code). That writes the provider's native
27
+ credential file:
28
+ Codex -> $CODEX_HOME/auth.json (default ~/.codex/auth.json)
29
+ Claude -> ~/.claude/.credentials.json (or the macOS Keychain)
30
+ 2. We read that file verbatim and inject it into the **same native path inside
31
+ the user's private sandbox** — so the sandbox's codex finds its token exactly
32
+ where it natively looks. No proxy, no federation: the sandbox refreshes the
33
+ token itself against the provider, just like a local install would.
34
+
35
+ This module is pure (stdlib only) and side-effect-light so it is unit-testable; the
36
+ network/forwarding lives in the CLI layer (``cli_accesscontrol.py``).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import base64
42
+ import datetime
43
+ import json
44
+ import os
45
+ import re
46
+ import sys
47
+ from pathlib import Path
48
+ from typing import Callable, Optional
49
+
50
+ # ── Codex / ChatGPT facts ────────────────────────────────────────────────────
51
+ CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" # codex CLI's public OAuth client (the id_token aud)
52
+ CODEX_OAUTH_NAMESPACE = "https://api.openai.com/auth" # claim namespace holding the ChatGPT plan
53
+ DEFAULT_SANDBOX_CODEX_HOME = "/home/gem/.codex" # informational; the shell resolves ${CODEX_HOME:-$HOME/.codex}
54
+ CODEX_INJECT_MARKER = "AGENTKIT_CODEX_INJECTED"
55
+ CLAUDE_INJECT_MARKER = "AGENTKIT_CLAUDE_INJECTED"
56
+ CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials"
57
+
58
+ PROVIDERS = ("codex", "claude")
59
+
60
+
61
+ class ModelLoginError(RuntimeError):
62
+ """A user-facing failure resolving or injecting a model subscription token."""
63
+
64
+
65
+ # ── small helpers ────────────────────────────────────────────────────────────
66
+ def b64(data: str | bytes) -> str:
67
+ """Standard base64 (single line, no newlines) — safe to embed in a shell ``printf``."""
68
+ if isinstance(data, str):
69
+ data = data.encode("utf-8")
70
+ return base64.b64encode(data).decode("ascii")
71
+
72
+
73
+ def _iso(epoch_seconds: int) -> str:
74
+ return datetime.datetime.fromtimestamp(epoch_seconds, tz=datetime.timezone.utc).isoformat()
75
+
76
+
77
+ def decode_jwt_claims(jwt: str) -> dict:
78
+ """Decode (NOT verify) a JWT's payload claims. We only read non-secret metadata."""
79
+ parts = (jwt or "").split(".")
80
+ if len(parts) < 2:
81
+ raise ModelLoginError("malformed JWT")
82
+ seg = parts[1]
83
+ try:
84
+ return json.loads(base64.urlsafe_b64decode(seg + "=" * (-len(seg) % 4)))
85
+ except Exception as exc: # noqa: BLE001
86
+ raise ModelLoginError(f"cannot decode JWT claims: {exc}") from exc
87
+
88
+
89
+ # ── Codex (ChatGPT) ──────────────────────────────────────────────────────────
90
+ def codex_home_path(explicit: Optional[str] = None) -> Path:
91
+ """Resolve the LOCAL codex home: --codex-home > $CODEX_HOME > ~/.codex."""
92
+ if explicit:
93
+ return Path(explicit).expanduser()
94
+ env = os.environ.get("CODEX_HOME")
95
+ if env:
96
+ return Path(env).expanduser()
97
+ return Path.home() / ".codex"
98
+
99
+
100
+ def read_codex_auth(path: str | Path) -> dict:
101
+ p = Path(path)
102
+ try:
103
+ data = json.loads(p.read_text(encoding="utf-8"))
104
+ except FileNotFoundError as exc:
105
+ raise ModelLoginError(f"codex auth file not found: {p}") from exc
106
+ except (json.JSONDecodeError, OSError) as exc:
107
+ raise ModelLoginError(f"cannot read codex auth file {p}: {exc}") from exc
108
+ if not isinstance(data, dict):
109
+ raise ModelLoginError(f"codex auth file {p} is not a JSON object")
110
+ return data
111
+
112
+
113
+ def validate_codex_auth(data: dict) -> None:
114
+ """A usable codex auth has ChatGPT tokens (SSO) or an API key."""
115
+ tokens = data.get("tokens")
116
+ has_tokens = isinstance(tokens, dict) and bool(tokens.get("id_token") or tokens.get("access_token"))
117
+ has_key = bool(data.get("OPENAI_API_KEY"))
118
+ if not (has_tokens or has_key):
119
+ raise ModelLoginError(
120
+ "codex auth.json has neither ChatGPT tokens nor an API key — run `codex login` first"
121
+ )
122
+
123
+
124
+ def codex_auth_summary(data: dict) -> dict:
125
+ """A redacted, secret-free summary (provider, plan, account, expiry) for display."""
126
+ tokens = data.get("tokens") or {}
127
+ summary: dict = {
128
+ "provider": "codex (ChatGPT)",
129
+ "auth_mode": data.get("auth_mode") or ("chatgpt" if tokens else "apikey"),
130
+ "has_refresh_token": bool(tokens.get("refresh_token")),
131
+ "account_id": tokens.get("account_id"),
132
+ }
133
+ idt = tokens.get("id_token")
134
+ if idt:
135
+ try:
136
+ claims = decode_jwt_claims(idt)
137
+ except ModelLoginError:
138
+ claims = {}
139
+ ns = claims.get(CODEX_OAUTH_NAMESPACE) or {}
140
+ summary["email"] = claims.get("email")
141
+ summary["plan"] = ns.get("chatgpt_plan_type")
142
+ summary["chatgpt_account_id"] = ns.get("chatgpt_account_id") or summary.get("account_id")
143
+ exp = claims.get("exp")
144
+ if isinstance(exp, int):
145
+ summary["id_token_expires"] = _iso(exp)
146
+ summary["id_token_expired"] = exp < int(datetime.datetime.now(datetime.timezone.utc).timestamp())
147
+ return {k: v for k, v in summary.items() if v is not None}
148
+
149
+
150
+ def run_codex_login(
151
+ *, codex_home: Optional[Path] = None, codex_bin: str = "codex", timeout: int = 300
152
+ ) -> None:
153
+ """Trigger codex's native local SSO (opens a browser) so it writes a fresh auth.json."""
154
+ import subprocess
155
+
156
+ env = dict(os.environ)
157
+ if codex_home:
158
+ env["CODEX_HOME"] = str(codex_home)
159
+ try:
160
+ subprocess.run([codex_bin, "login"], env=env, timeout=timeout, check=True) # noqa: S603
161
+ except FileNotFoundError as exc:
162
+ raise ModelLoginError(
163
+ f"`{codex_bin}` not found on PATH — install the Codex CLI and run `codex login`, "
164
+ f"or pass --auth-file <path-to-your-auth.json>"
165
+ ) from exc
166
+ except subprocess.CalledProcessError as exc:
167
+ raise ModelLoginError(f"`codex login` failed (exit {exc.returncode})") from exc
168
+ except subprocess.TimeoutExpired as exc:
169
+ raise ModelLoginError("`codex login` timed out") from exc
170
+
171
+
172
+ def resolve_local_codex_auth(
173
+ *,
174
+ codex_home: Optional[str] = None,
175
+ auth_file: Optional[str] = None,
176
+ allow_login: bool = True,
177
+ login_timeout: int = 300,
178
+ login_runner: Optional[Callable[..., None]] = None,
179
+ ) -> tuple[Path, dict]:
180
+ """Return ``(path, data)`` of a usable LOCAL codex auth, running `codex login` if needed."""
181
+ if auth_file:
182
+ path = Path(auth_file).expanduser()
183
+ if not path.exists():
184
+ raise ModelLoginError(f"--auth-file not found: {path}")
185
+ data = read_codex_auth(path)
186
+ validate_codex_auth(data)
187
+ return path, data
188
+
189
+ home = codex_home_path(codex_home)
190
+ path = home / "auth.json"
191
+ if not path.exists():
192
+ if not allow_login:
193
+ raise ModelLoginError(
194
+ f"no codex auth at {path} — run `codex login` (or pass --auth-file / --no-login off)"
195
+ )
196
+ (login_runner or run_codex_login)(codex_home=home, timeout=login_timeout)
197
+ if not path.exists():
198
+ raise ModelLoginError(f"`codex login` finished but {path} is still missing")
199
+ data = read_codex_auth(path)
200
+ validate_codex_auth(data)
201
+ return path, data
202
+
203
+
204
+ # ── Codex config.toml: switch the sandbox codex to the ChatGPT subscription ──
205
+ _TOP_MODEL_PIN = re.compile(r"^\s*(model|model_provider|review_model)\s*=")
206
+ _AUTH_METHOD = re.compile(r"^\s*preferred_auth_method\s*=")
207
+ _TABLE_HEADER = re.compile(r"^\s*\[")
208
+
209
+
210
+ def rewrite_codex_config_for_chatgpt(toml_text: str) -> str:
211
+ """Switch an existing codex config.toml to ChatGPT-subscription auth, preserving everything else.
212
+
213
+ The sandbox's default config pins a custom ``model_provider``/``model`` to a Volcengine Ark
214
+ endpoint that authenticates with an API key (``env_key``); in that mode codex never looks at the
215
+ injected ChatGPT token. To use the subscription we:
216
+ * drop the top-level ``model`` / ``model_provider`` / ``review_model`` pins, so codex falls back
217
+ to its built-in ``openai`` provider and the subscription's default model, and
218
+ * force ``preferred_auth_method = "chatgpt"`` so the OAuth token wins over any stray API key.
219
+ Tables (``[tui]``, ``[projects.*]``, ``[mcp_servers.*]``, ``[model_providers.*]``) and other
220
+ top-level keys (approval_policy, sandbox_mode, model_reasoning_effort, …) are kept verbatim.
221
+ """
222
+ out: list[str] = []
223
+ seen_table = False
224
+ for line in toml_text.splitlines():
225
+ if _TABLE_HEADER.match(line):
226
+ seen_table = True
227
+ if _AUTH_METHOD.match(line):
228
+ continue # re-added at the top
229
+ if not seen_table and _TOP_MODEL_PIN.match(line):
230
+ continue
231
+ out.append(line)
232
+ body = "\n".join(out).strip("\n")
233
+ return 'preferred_auth_method = "chatgpt"\n' + (body + "\n" if body else "")
234
+
235
+
236
+ def minimal_chatgpt_codex_config() -> str:
237
+ """A config.toml for a sandbox that has no codex config yet — ChatGPT auth, headless-friendly."""
238
+ return "\n".join(
239
+ [
240
+ 'preferred_auth_method = "chatgpt"',
241
+ 'approval_policy = "never"',
242
+ 'sandbox_mode = "danger-full-access"',
243
+ "",
244
+ '[projects."/home/gem"]',
245
+ 'trust_level = "trusted"',
246
+ "",
247
+ ]
248
+ )
249
+
250
+
251
+ def read_codex_config_command() -> str:
252
+ """Shell command that prints the sandbox's current codex config.toml (empty if none)."""
253
+ return 'cat "${CODEX_HOME:-$HOME/.codex}/config.toml" 2>/dev/null || true'
254
+
255
+
256
+ def build_codex_injection_command(*, auth_json: str, config_toml: Optional[str] = None) -> str:
257
+ """A single POSIX-sh command that writes auth.json (and optionally config.toml) into the
258
+ sandbox's native codex home (``${CODEX_HOME:-$HOME/.codex}``), 0600, and prints a marker."""
259
+ lines = [
260
+ "set -e",
261
+ 'CH="${CODEX_HOME:-$HOME/.codex}"',
262
+ 'mkdir -p "$CH"',
263
+ "umask 077",
264
+ f"printf %s '{b64(auth_json)}' | base64 -d > \"$CH/auth.json\"",
265
+ 'chmod 600 "$CH/auth.json"',
266
+ ]
267
+ if config_toml is not None:
268
+ lines.append(f"printf %s '{b64(config_toml)}' | base64 -d > \"$CH/config.toml\"")
269
+ lines.append(f'echo "{CODEX_INJECT_MARKER} $CH"')
270
+ return "\n".join(lines)
271
+
272
+
273
+ # ── Claude Code ──────────────────────────────────────────────────────────────
274
+ def claude_creds_path(explicit: Optional[str] = None) -> Path:
275
+ if explicit:
276
+ return Path(explicit).expanduser()
277
+ return Path.home() / ".claude" / ".credentials.json"
278
+
279
+
280
+ def _read_macos_keychain(service: str) -> Optional[str]:
281
+ import subprocess
282
+
283
+ try:
284
+ out = subprocess.run( # noqa: S603
285
+ ["security", "find-generic-password", "-s", service, "-w"], # noqa: S607
286
+ capture_output=True,
287
+ text=True,
288
+ timeout=10,
289
+ )
290
+ except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
291
+ return None
292
+ if out.returncode != 0:
293
+ return None
294
+ return (out.stdout or "").strip() or None
295
+
296
+
297
+ def read_claude_creds(*, creds_file: Optional[str] = None, allow_keychain: bool = True) -> dict:
298
+ """Read Claude Code subscription creds from the file, or the macOS Keychain as a fallback."""
299
+ path = claude_creds_path(creds_file)
300
+ if path.exists():
301
+ try:
302
+ data = json.loads(path.read_text(encoding="utf-8"))
303
+ except (json.JSONDecodeError, OSError) as exc:
304
+ raise ModelLoginError(f"cannot read {path}: {exc}") from exc
305
+ elif creds_file:
306
+ raise ModelLoginError(f"--auth-file not found: {path}")
307
+ elif allow_keychain and sys.platform == "darwin":
308
+ raw = _read_macos_keychain(CLAUDE_KEYCHAIN_SERVICE)
309
+ if not raw:
310
+ raise ModelLoginError(
311
+ "no Claude Code credentials in ~/.claude/.credentials.json or the macOS Keychain — "
312
+ "run `claude` and log in with your subscription first"
313
+ )
314
+ try:
315
+ data = json.loads(raw)
316
+ except json.JSONDecodeError as exc:
317
+ raise ModelLoginError("Claude Keychain entry is not valid JSON") from exc
318
+ else:
319
+ raise ModelLoginError(
320
+ "no Claude Code credentials at ~/.claude/.credentials.json — "
321
+ "run `claude` and log in with your subscription first"
322
+ )
323
+ if not isinstance(data, dict):
324
+ raise ModelLoginError("Claude credentials file is not a JSON object")
325
+ return data
326
+
327
+
328
+ def validate_claude_creds(data: dict) -> None:
329
+ oauth = data.get("claudeAiOauth")
330
+ if not (isinstance(oauth, dict) and oauth.get("accessToken")):
331
+ raise ModelLoginError("Claude credentials missing claudeAiOauth.accessToken")
332
+
333
+
334
+ def claude_creds_summary(data: dict) -> dict:
335
+ oauth = data.get("claudeAiOauth") or {}
336
+ summary: dict = {
337
+ "provider": "claude (Claude Code)",
338
+ "subscription": oauth.get("subscriptionType"),
339
+ "scopes": oauth.get("scopes"),
340
+ "has_refresh_token": bool(oauth.get("refreshToken")),
341
+ }
342
+ exp = oauth.get("expiresAt")
343
+ if isinstance(exp, (int, float)):
344
+ summary["access_token_expires"] = _iso(int(exp / 1000)) # Claude stores epoch milliseconds
345
+ return {k: v for k, v in summary.items() if v is not None}
346
+
347
+
348
+ def build_claude_injection_command(*, creds_json: str) -> str:
349
+ """A single POSIX-sh command that writes Claude Code creds into ``$HOME/.claude``, 0600."""
350
+ return "\n".join(
351
+ [
352
+ "set -e",
353
+ 'CD="$HOME/.claude"',
354
+ 'mkdir -p "$CD"',
355
+ "umask 077",
356
+ f"printf %s '{b64(creds_json)}' | base64 -d > \"$CD/.credentials.json\"",
357
+ 'chmod 600 "$CD/.credentials.json"',
358
+ f'echo "{CLAUDE_INJECT_MARKER} $CD"',
359
+ ]
360
+ )
@@ -18,9 +18,12 @@ This is the top-level base class for all service clients.
18
18
  """
19
19
 
20
20
  import json
21
+ import os
21
22
  from typing import Any, Dict, Type, TypeVar, Union, Optional
22
23
  from dataclasses import dataclass
23
24
 
25
+ from requests.adapters import HTTPAdapter
26
+ from urllib3.util.retry import Retry
24
27
  from volcengine.ApiInfo import ApiInfo
25
28
  from volcengine.base.Service import Service
26
29
  from volcengine.Credentials import Credentials as VolcCredentials
@@ -174,6 +177,41 @@ class BaseServiceClient(Service):
174
177
  if self.session_token:
175
178
  self.set_session_token(self.session_token)
176
179
 
180
+ self._install_retry_adapter()
181
+
182
+ def _install_retry_adapter(self) -> None:
183
+ """Retry transient overload responses (429/503) and connection failures
184
+ with exponential backoff on the shared ``Service`` session.
185
+
186
+ Scoped to cases that mean the request was not processed, so
187
+ non-idempotent ``Create*`` actions are never double-executed: read
188
+ timeouts are not retried (``read=0``) and only 429/503 are status-
189
+ retried. Honors ``Retry-After``. Disabled with AGENTKIT_HTTP_RETRIES=0.
190
+ """
191
+ try:
192
+ retries = max(0, int(os.getenv("AGENTKIT_HTTP_RETRIES", "2")))
193
+ except ValueError:
194
+ retries = 2
195
+ if retries <= 0:
196
+ return
197
+ session = getattr(self, "session", None)
198
+ if session is None:
199
+ return
200
+ retry = Retry(
201
+ total=retries,
202
+ connect=retries,
203
+ read=0,
204
+ status=retries,
205
+ status_forcelist=(429, 503),
206
+ allowed_methods=None, # OpenAPI calls are POST; gate by status only
207
+ backoff_factor=0.5,
208
+ respect_retry_after_header=True,
209
+ raise_on_status=False,
210
+ )
211
+ adapter = HTTPAdapter(max_retries=retry)
212
+ session.mount("https://", adapter)
213
+ session.mount("http://", adapter)
214
+
177
215
  def _should_auto_refresh_vefaas_credentials(self) -> bool:
178
216
  if self._explicit_credentials:
179
217
  return False
@@ -26,6 +26,7 @@ auth block), serialized as a layered JSON document::
26
26
  "tools": ["web_search"],
27
27
  "skills": [],
28
28
  "system_prompt": "You are a helpful assistant.",
29
+ "description": "A helpful assistant.",
29
30
  "runtime": "adk",
30
31
  "knowledgebase": {"type": "viking", "project": "...", "region": "..."},
31
32
  "long_term_memory": {"type": ""},
@@ -42,7 +43,7 @@ import os
42
43
  import re
43
44
  from pathlib import Path
44
45
  from typing import Any, Optional
45
- from urllib.parse import parse_qs, urlparse
46
+ from urllib.parse import parse_qs, urlparse, urlunparse
46
47
 
47
48
  import typer
48
49
  from rich.console import Console
@@ -74,6 +75,10 @@ _REGISTRY_QUERY_KEYS = {
74
75
  _REGISTRY_INT_KEYS = {"top_k"}
75
76
  _REGISTER_NETWORK_TYPES = {"public", "private"}
76
77
  _REGISTER_DEFAULT_VERSION = "2025-10-30"
78
+ _DEFAULT_A2A_REGISTRY_URI = (
79
+ "agentkit://a2a-registry?"
80
+ "space_name=Default&region=cn-beijing&endpoint=https://open.volcengineapi.com/"
81
+ )
77
82
 
78
83
 
79
84
  class _A2ARegisterError(Exception):
@@ -96,6 +101,17 @@ def _is_blank(value: object) -> bool:
96
101
  return value is None or value == "" or value == [] or value == {}
97
102
 
98
103
 
104
+ def _derive_agent_name(harness_name: str) -> str:
105
+ """ADK agent name derived from a harness name.
106
+
107
+ Must mirror veadk's ``harness_app/utils.py::agent_name_from_harness``.
108
+ """
109
+ name = re.sub(r"[^0-9A-Za-z_]", "_", harness_name or "")
110
+ if not name or name[0].isdigit():
111
+ name = f"_{name}"
112
+ return f"{name}_" if name == "user" else name
113
+
114
+
99
115
  def _prune(data: dict) -> None:
100
116
  """Drop unset fields so the file holds only what is configured.
101
117
 
@@ -142,9 +158,16 @@ def _parse_registry_int(key: str, value: object) -> object:
142
158
  raise ValueError(f"Registry param `{key}` must be an integer, got {value!r}.") from exc
143
159
 
144
160
 
161
+ def _expand_default_registry_uri(value: str) -> str:
162
+ raw = value.strip()
163
+ if raw.lower() == "default":
164
+ return _DEFAULT_A2A_REGISTRY_URI
165
+ return raw
166
+
167
+
145
168
  def _parse_registry_uri(value: str) -> dict:
146
169
  """Parse the supported AgentKit A2A registry URI into a spec section."""
147
- raw = value.strip()
170
+ raw = _expand_default_registry_uri(value)
148
171
  if raw.lower() == "disabled":
149
172
  return {"type": ""}
150
173
 
@@ -157,7 +180,7 @@ def _parse_registry_uri(value: str) -> dict:
157
180
  raise ValueError(
158
181
  "Unsupported registry URI. Currently only "
159
182
  "`agentkit://a2a-registry?space_id=xxx&top_k=3` or "
160
- "`disabled` is supported."
183
+ "`default` / `disabled` is supported."
161
184
  )
162
185
 
163
186
  query = {
@@ -211,8 +234,13 @@ def _apply_registry_config(
211
234
  section = {}
212
235
 
213
236
  if registry is not None:
214
- section.update(_parse_registry_uri(registry))
237
+ parsed_registry = _parse_registry_uri(registry)
238
+ if parsed_registry.get("space_name") and "space_id" not in parsed_registry:
239
+ section.pop("space_id", None)
240
+ section.update(parsed_registry)
215
241
 
242
+ if registry_space_name is not None:
243
+ section.pop("space_id", None)
216
244
  _set_registry_value(section, "space_id", registry_space_id)
217
245
  _set_registry_value(section, "space_name", registry_space_name)
218
246
  _set_registry_value(section, "top_k", registry_top_k)
@@ -253,6 +281,16 @@ def _apply_registry_config(
253
281
  )
254
282
 
255
283
  data["registry"] = section
284
+ if section.get("type") == "agentkit_a2a":
285
+ resolved_endpoint, resolved_region = _resolve_agentkit_openapi_target(
286
+ endpoint=str(section["endpoint"]) if section.get("endpoint") else None,
287
+ region=str(section["region"]) if section.get("region") else None,
288
+ )
289
+ _enable_a2a_space_intent(
290
+ str(section["space_id"]),
291
+ endpoint=resolved_endpoint,
292
+ region=resolved_region,
293
+ )
256
294
 
257
295
 
258
296
  def _load_spec(path: Path) -> dict:
@@ -294,6 +332,30 @@ def _default_agentkit_endpoint(region: str) -> str:
294
332
  return f"https://agentkit.{region}.volcengineapi.com/"
295
333
 
296
334
 
335
+ def _resolve_agentkit_openapi_target(
336
+ *,
337
+ endpoint: Optional[str],
338
+ region: Optional[str],
339
+ ) -> tuple[str, str]:
340
+ resolved_region = (
341
+ region
342
+ or os.getenv("AGENTKIT_REGION")
343
+ or os.getenv("VOLCENGINE_REGION")
344
+ or "cn-beijing"
345
+ )
346
+ resolved_endpoint = endpoint or _default_agentkit_endpoint(str(resolved_region))
347
+ parsed = urlparse(resolved_endpoint)
348
+ if (
349
+ parsed.scheme
350
+ and parsed.netloc
351
+ and (parsed.query or parsed.params or parsed.fragment)
352
+ ):
353
+ resolved_endpoint = urlunparse(
354
+ (parsed.scheme, parsed.netloc, parsed.path or "/", "", "", "")
355
+ )
356
+ return resolved_endpoint, str(resolved_region)
357
+
358
+
297
359
  def _request_id(response: dict[str, Any]) -> str | None:
298
360
  return (response.get("ResponseMetadata") or {}).get("RequestId")
299
361
 
@@ -374,6 +436,25 @@ def _agentkit_post(
374
436
  return data, duration_ms
375
437
 
376
438
 
439
+ def _enable_a2a_space_intent(
440
+ space_id: str,
441
+ *,
442
+ endpoint: str,
443
+ region: str,
444
+ ) -> dict[str, Any]:
445
+ response, request_duration_ms = _agentkit_post(
446
+ endpoint=endpoint,
447
+ version=_REGISTER_DEFAULT_VERSION,
448
+ region=region,
449
+ action="UpdateA2aSpace",
450
+ body={"Id": space_id, "IntentEnabled": True},
451
+ )
452
+ return {
453
+ "request_id": _request_id(response),
454
+ "request_duration_ms": request_duration_ms,
455
+ }
456
+
457
+
377
458
  def _resolve_a2a_space_id_by_name(
378
459
  space_name: str,
379
460
  *,
@@ -587,6 +668,12 @@ def harness_command(
587
668
  system_prompt: Optional[str] = typer.Option(
588
669
  None, "--system-prompt", help="Agent system prompt / instruction."
589
670
  ),
671
+ description: Optional[str] = typer.Option(
672
+ None,
673
+ "--description",
674
+ "--desc",
675
+ help="Agent description (used at agent init, e.g. for A2A discovery).",
676
+ ),
590
677
  model_name: Optional[str] = typer.Option(
591
678
  None, "--model-name", help="Reasoning model name."
592
679
  ),
@@ -612,7 +699,10 @@ def harness_command(
612
699
  registry: Optional[str] = typer.Option(
613
700
  None,
614
701
  "--registry",
615
- help='AgentKit A2A registry URI, e.g. "agentkit://a2a-registry?space_id=xxx&top_k=3".',
702
+ help=(
703
+ 'AgentKit A2A registry URI, "default", or "disabled", e.g. '
704
+ '"agentkit://a2a-registry?space_id=xxx&top_k=3".'
705
+ ),
616
706
  ),
617
707
  registry_space_id: Optional[str] = typer.Option(
618
708
  None, "--registry-space-id", help="AgentKit A2A SpaceId."
@@ -790,6 +880,13 @@ def harness_command(
790
880
  )
791
881
  raise typer.Exit(1)
792
882
 
883
+ agent_name = _derive_agent_name(name)
884
+ if agent_name != name:
885
+ console.print(
886
+ f"[yellow]ℹ Once deployed, the agent will be named "
887
+ f"'{agent_name}' instead of '{name}'.[/yellow]"
888
+ )
889
+
793
890
  _validate_choice("--runtime", runtime, _RUNTIMES)
794
891
  _validate_choice("--knowledgebase-type", knowledgebase_type, _KNOWLEDGEBASE_TYPES)
795
892
  _validate_choice(
@@ -820,6 +917,8 @@ def harness_command(
820
917
  data["model"]["name"] = model_name
821
918
  if system_prompt is not None:
822
919
  data["system_prompt"] = system_prompt
920
+ if description is not None:
921
+ data["description"] = description
823
922
  if runtime is not None:
824
923
  data["runtime"] = runtime
825
924
  if structured_tool_calls is not None:
@@ -904,7 +1003,9 @@ def harness_command(
904
1003
  registry_region,
905
1004
  )
906
1005
  except _A2ARegisterError as exc:
907
- console.print(f"[red]Error: failed to resolve A2A space name: {exc.message}[/red]")
1006
+ console.print(
1007
+ f"[red]Error: failed to configure A2A registry: {exc.message}[/red]"
1008
+ )
908
1009
  if exc.diagnostics:
909
1010
  console.print(json.dumps(exc.diagnostics, ensure_ascii=False, indent=2))
910
1011
  raise typer.Exit(1) from exc
@@ -662,6 +662,7 @@ def _parse_harness_registry_override(value: Optional[str]) -> dict[str, Any]:
662
662
  """Parse ``--registry`` into one-time harness registry overrides.
663
663
 
664
664
  Supported forms:
665
+ - default (Default space in cn-beijing via https://open.volcengineapi.com/)
665
666
  - agentkit://a2a-registry?space_id=xxx&top_k=3&region=cn-beijing
666
667
  - https://... (treated as registry_endpoint; recognized query params are
667
668
  also extracted when present)
@@ -673,6 +674,9 @@ def _parse_harness_registry_override(value: Optional[str]) -> dict[str, Any]:
673
674
  if not raw:
674
675
  return {}
675
676
 
677
+ from agentkit.toolkit.cli.cli_add import _expand_default_registry_uri
678
+
679
+ raw = _expand_default_registry_uri(raw)
676
680
  parsed = urlparse(raw)
677
681
  overrides: dict[str, Any] = {}
678
682
 
@@ -686,14 +690,16 @@ def _parse_harness_registry_override(value: Optional[str]) -> dict[str, Any]:
686
690
  if parsed.netloc != "a2a-registry" or parsed.path not in {"", "/"}:
687
691
  raise ValueError(
688
692
  "Unsupported registry URI. Use "
689
- '`agentkit://a2a-registry?space_id=xxx&top_k=3` or an http(s) URL.'
693
+ '`default`, `agentkit://a2a-registry?space_id=xxx&top_k=3`, '
694
+ "or an http(s) URL."
690
695
  )
691
696
  elif parsed.scheme in {"http", "https"}:
692
697
  overrides["registry_endpoint"] = raw
693
698
  else:
694
699
  raise ValueError(
695
700
  "Unsupported registry value. Use "
696
- '`agentkit://a2a-registry?space_id=xxx&top_k=3` or an http(s) URL.'
701
+ '`default`, `agentkit://a2a-registry?space_id=xxx&top_k=3`, '
702
+ "or an http(s) URL."
697
703
  )
698
704
 
699
705
  unknown = sorted(set(query) - set(_INVOKE_REGISTRY_QUERY_ALIASES))
@@ -786,6 +792,23 @@ def _merge_harness_registry_overrides(
786
792
  return overrides
787
793
 
788
794
 
795
+ def _enable_harness_registry_intent(overrides: dict[str, Any]) -> None:
796
+ space_id = overrides.get("registry_space_id")
797
+ if not space_id:
798
+ return
799
+
800
+ from agentkit.toolkit.cli.cli_add import (
801
+ _enable_a2a_space_intent,
802
+ _resolve_agentkit_openapi_target,
803
+ )
804
+
805
+ endpoint, region = _resolve_agentkit_openapi_target(
806
+ endpoint=overrides.get("registry_endpoint"),
807
+ region=overrides.get("registry_region"),
808
+ )
809
+ _enable_a2a_space_intent(str(space_id), endpoint=endpoint, region=region)
810
+
811
+
789
812
  # Fixed ADK app name for the run_sse path. The harness loader serves its single
790
813
  # agent under any app name, so a stable constant keeps the CLI decoupled from the
791
814
  # deployed HARNESS_NAME.
@@ -998,7 +1021,8 @@ def harness_command(
998
1021
  "--registry",
999
1022
  help=(
1000
1023
  "Override A2A registry for this invocation. Accepts "
1001
- "`agentkit://a2a-registry?space_id=xxx&top_k=3` or an http(s) URL."
1024
+ "`default`, `agentkit://a2a-registry?space_id=xxx&top_k=3`, "
1025
+ "or an http(s) URL."
1002
1026
  ),
1003
1027
  ),
1004
1028
  registry_top_k: int = typer.Option(
@@ -1066,8 +1090,9 @@ def harness_command(
1066
1090
  registry_endpoint=registry_endpoint,
1067
1091
  registry_region=registry_region,
1068
1092
  )
1093
+ _enable_harness_registry_intent(registry_overrides)
1069
1094
  except _A2ARegisterError as e:
1070
- console.print(f"[red]Error: failed to resolve A2A space name: {e}[/red]")
1095
+ console.print(f"[red]Error: failed to configure A2A registry: {e}[/red]")
1071
1096
  raise typer.Exit(1)
1072
1097
  except ValueError as e:
1073
1098
  console.print(f"[red]Error: {e}[/red]")
@@ -22,6 +22,7 @@ from agentkit.toolkit.cli.sandbox.cli_create import create_command
22
22
  from agentkit.toolkit.cli.sandbox.cli_exec import exec_command
23
23
  from agentkit.toolkit.cli.sandbox.cli_file import file_command
24
24
  from agentkit.toolkit.cli.sandbox.cli_get import get_command
25
+ from agentkit.toolkit.cli.sandbox.cli_model_login import codex_login_command
25
26
  from agentkit.toolkit.cli.sandbox.cli_mount import mount_command
26
27
  from agentkit.toolkit.cli.sandbox.cli_run import run_command
27
28
  from agentkit.toolkit.cli.sandbox.cli_shell import shell_command
@@ -46,4 +47,6 @@ sandbox_app.command(
46
47
  context_settings={"allow_extra_args": True},
47
48
  )(shell_command)
48
49
  sandbox_app.command(name="web")(web_command)
50
+ sandbox_app.command(name="codex-login")(codex_login_command)
51
+ sandbox_app.command(name="model-login")(codex_login_command) # provider-agnostic alias
49
52
  sandbox_app.add_typer(file_command, name="file")
@@ -0,0 +1,238 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """`agentkit sandbox codex-login` — bring your own model subscription into a sandbox.
16
+
17
+ Codex/ChatGPT and Claude Code are used via a JWT obtained by an interactive SSO login, not a
18
+ static API key. Codex supports that SSO **locally**; in the sandbox it is remote. So instead of
19
+ federating the platform UserPool with OpenAI/Anthropic, this command:
20
+
21
+ 1. reads the provider's native credential the local SSO already produced
22
+ (``$CODEX_HOME/auth.json`` for Codex, ``~/.claude/.credentials.json`` for Claude Code), and
23
+ 2. injects it into the **same native path inside the sandbox session**, so the sandbox's codex
24
+ finds its token exactly where it natively looks.
25
+
26
+ The sandbox refreshes the token itself against the provider, just like a local install would. The
27
+ heavy lifting (resolve / validate / redact / config rewrite / build the injection command) lives in
28
+ ``agentkit.auth.model_login``; this file is only the sandbox-session transport + CLI surface.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import re
35
+ from typing import Optional
36
+
37
+ import typer
38
+
39
+ from agentkit.toolkit.cli.sandbox.cli_file import _exec_shell_command
40
+ from agentkit.toolkit.cli.sandbox.session_create import (
41
+ SANDBOX_TOOL_ID_ENV,
42
+ ensure_sandbox_session,
43
+ )
44
+ from agentkit.toolkit.cli.sandbox.tool_resolve import SandboxToolType
45
+ from agentkit.toolkit.cli.sandbox.sandbox_client import error
46
+
47
+
48
+ def _shell_output(payload: dict) -> str:
49
+ data = payload.get("data")
50
+ if isinstance(data, dict):
51
+ out = data.get("output")
52
+ if isinstance(out, str):
53
+ return out
54
+ return ""
55
+
56
+
57
+ def _redact_inject(cmd: str) -> str:
58
+ """Redact the base64 token blob in an injection command (for --dry-run display)."""
59
+ return re.sub(
60
+ r"printf %s '([A-Za-z0-9+/=]+)'",
61
+ lambda m: f"printf %s '<base64 {len(m.group(1))} bytes — redacted>'",
62
+ cmd,
63
+ )
64
+
65
+
66
+ def codex_login_command(
67
+ session_id: Optional[str] = typer.Option(
68
+ None,
69
+ "--session-id",
70
+ "--sid",
71
+ "-s",
72
+ help=(
73
+ "Sandbox session to inject into. Defaults to a new session; reuse the printed id "
74
+ "with `agentkit sandbox exec --sid <id>`."
75
+ ),
76
+ ),
77
+ provider: str = typer.Option(
78
+ "codex",
79
+ "--provider",
80
+ "-p",
81
+ help="Subscription to bring in: codex (ChatGPT) or claude (Claude Code).",
82
+ ),
83
+ auth_file: Optional[str] = typer.Option(
84
+ None,
85
+ "--auth-file",
86
+ help=(
87
+ "Use a specific local credential file (codex auth.json / claude .credentials.json) "
88
+ "instead of running the local SSO."
89
+ ),
90
+ ),
91
+ codex_home: Optional[str] = typer.Option(
92
+ None,
93
+ "--codex-home",
94
+ help="Local codex home (default $CODEX_HOME or ~/.codex).",
95
+ ),
96
+ login: bool = typer.Option(
97
+ True,
98
+ "--login/--no-login",
99
+ help="If no local credential exists, trigger `codex login` (browser SSO) once.",
100
+ ),
101
+ keep_model_config: bool = typer.Option(
102
+ False,
103
+ "--keep-model-config",
104
+ help="Only inject the token; do not switch the sandbox codex config to subscription auth.",
105
+ ),
106
+ tool_id: Optional[str] = typer.Option(
107
+ None,
108
+ "--tool-id",
109
+ help=f"Sandbox tool ID. Defaults to {SANDBOX_TOOL_ID_ENV}.",
110
+ ),
111
+ tool_type: SandboxToolType = typer.Option(
112
+ SandboxToolType.CODE_ENV,
113
+ "--tool-type",
114
+ help="Sandbox tool type to resolve when --tool-id is omitted.",
115
+ ),
116
+ dry_run: bool = typer.Option(
117
+ False,
118
+ "--dry-run",
119
+ help="Resolve the local credential and print what would be injected (token redacted); no session.",
120
+ ),
121
+ ) -> None:
122
+ """Inject your own ChatGPT/Codex or Claude Code subscription token into a sandbox session.
123
+
124
+ Run the provider's local SSO once (codex supports it locally), then this injects the native
125
+ token into the sandbox's native location so the sandbox codex runs on your subscription.
126
+ """
127
+ from agentkit.auth import model_login as ml
128
+
129
+ provider = (provider or "codex").lower()
130
+ if provider not in ml.PROVIDERS:
131
+ error(f"unknown provider: {provider} (supported: {', '.join(ml.PROVIDERS)})")
132
+
133
+ # 1) resolve the LOCAL native credential (runs the provider's local SSO if needed)
134
+ try:
135
+ if provider == "codex":
136
+ src, data = ml.resolve_local_codex_auth(
137
+ codex_home=codex_home, auth_file=auth_file, allow_login=login
138
+ )
139
+ summary = ml.codex_auth_summary(data)
140
+ else: # claude
141
+ data = ml.read_claude_creds(creds_file=auth_file)
142
+ ml.validate_claude_creds(data)
143
+ src = ml.claude_creds_path(auth_file)
144
+ summary = ml.claude_creds_summary(data)
145
+ except ml.ModelLoginError as exc:
146
+ error(str(exc))
147
+
148
+ typer.secho(f"\nLocal credential: {src}", fg=typer.colors.CYAN, err=True)
149
+ typer.echo(json.dumps(summary, indent=2, ensure_ascii=False))
150
+ if summary.get("id_token_expired") and not summary.get("has_refresh_token"):
151
+ typer.secho(
152
+ " warning: token expired and has no refresh_token — re-run `codex login` locally first.",
153
+ fg=typer.colors.YELLOW,
154
+ err=True,
155
+ )
156
+
157
+ payload = json.dumps(data, ensure_ascii=False)
158
+
159
+ # 2) build the injection (codex also switches its config to subscription auth unless --keep-model-config)
160
+ if provider == "codex":
161
+ if dry_run:
162
+ typer.secho("\n— dry-run: read current sandbox codex config (read-only) —", fg=typer.colors.CYAN, err=True)
163
+ typer.secho(f" $ {ml.read_codex_config_command()}", err=True)
164
+ cmd = ml.build_codex_injection_command(
165
+ auth_json=payload,
166
+ config_toml=None if keep_model_config else ml.minimal_chatgpt_codex_config(),
167
+ )
168
+ typer.secho("\n— dry-run: would inject (token redacted) —", fg=typer.colors.CYAN, err=True)
169
+ typer.secho(_redact_inject(cmd), err=True)
170
+ raise typer.Exit(0)
171
+ marker = ml.CODEX_INJECT_MARKER
172
+ else: # claude
173
+ cmd = ml.build_claude_injection_command(creds_json=payload)
174
+ marker = ml.CLAUDE_INJECT_MARKER
175
+ if dry_run:
176
+ typer.secho("\n— dry-run: would inject (token redacted) —", fg=typer.colors.CYAN, err=True)
177
+ typer.secho(_redact_inject(cmd), err=True)
178
+ raise typer.Exit(0)
179
+
180
+ # 3) ensure the sandbox session, then inject over its shell-exec endpoint
181
+ try:
182
+ session = ensure_sandbox_session(
183
+ session_id=session_id,
184
+ tool_id=tool_id,
185
+ tool_type=tool_type.value,
186
+ )
187
+ except typer.Exit:
188
+ raise
189
+ except Exception as exc: # noqa: BLE001
190
+ error(str(exc))
191
+
192
+ sid = session.get("session_id")
193
+ if not isinstance(sid, str) or not sid:
194
+ error("sandbox session missing session_id")
195
+
196
+ if provider == "codex":
197
+ new_cfg = None
198
+ if not keep_model_config:
199
+ cur = _shell_output(
200
+ _exec_shell_command(session, ml.read_codex_config_command(), quiet_errors=True)
201
+ )
202
+ new_cfg = (
203
+ ml.rewrite_codex_config_for_chatgpt(cur)
204
+ if cur.strip()
205
+ else ml.minimal_chatgpt_codex_config()
206
+ )
207
+ cmd = ml.build_codex_injection_command(auth_json=payload, config_toml=new_cfg)
208
+
209
+ out = _shell_output(_exec_shell_command(session, cmd))
210
+ if marker not in out:
211
+ error(f"injection did not confirm (marker missing). sandbox output: {out[:200]}")
212
+ where = out.split(marker, 1)[1].strip() or "<sandbox>"
213
+
214
+ typer.secho(
215
+ f"\n✓ injected your {summary.get('provider', provider)} subscription into {where}",
216
+ fg=typer.colors.GREEN,
217
+ bold=True,
218
+ err=True,
219
+ )
220
+ typer.secho(f" session: {sid}", fg=typer.colors.CYAN, err=True)
221
+ if provider == "codex":
222
+ typer.secho(
223
+ f' next: agentkit sandbox exec --sid {sid} --command "codex" # codex runs on your subscription',
224
+ fg=typer.colors.CYAN,
225
+ err=True,
226
+ )
227
+ if not keep_model_config:
228
+ typer.secho(
229
+ " (switched the sandbox codex config to ChatGPT subscription auth; keep it with --keep-model-config)",
230
+ fg=typer.colors.BRIGHT_BLACK,
231
+ err=True,
232
+ )
233
+ typer.secho(
234
+ " note: the token is refreshed by codex/claude inside the sandbox; re-inject if the session is recreated.",
235
+ fg=typer.colors.BRIGHT_BLACK,
236
+ err=True,
237
+ )
238
+ typer.echo(json.dumps({"injected": True, "provider": provider, "session_id": sid, "path": where}, ensure_ascii=False))
@@ -585,8 +585,11 @@ class VeAgentkitRuntimeRunner(Runner):
585
585
  f"Generated role name: {config.runtime_role_name}"
586
586
  )
587
587
 
588
- # Generate API key name if not provided
589
- if (
588
+ # Generate API key name if not provided. Skipped for custom_jwt: the
589
+ # gateway authorizes via JWT and never uses an API key (see
590
+ # _build_authorizer_config_for_create), so generating one here only
591
+ # produced a misleading "Generated API key name" line.
592
+ if config.runtime_auth_type != AUTH_TYPE_CUSTOM_JWT and (
590
593
  config.runtime_apikey_name == AUTO_CREATE_VE
591
594
  or not config.runtime_apikey_name
592
595
  ):
agentkit/utils/ve_sign.py CHANGED
@@ -31,6 +31,7 @@ import hashlib
31
31
  import hmac
32
32
  import os
33
33
  import platform
34
+ import time
34
35
  import requests
35
36
  from urllib.parse import quote
36
37
 
@@ -46,6 +47,79 @@ Scheme = "https"
46
47
  MAX_X_CUSTOM_SOURCE_LENGTH = 256
47
48
 
48
49
 
50
+ # Transient-failure handling for signed OpenAPI calls. Historically this used a
51
+ # single timeout-less ``requests.request`` — a stalled connection could hang
52
+ # forever, and a transient overload (429/503) or connection error failed the
53
+ # call outright with no retry. Both are now bounded and conservatively retried.
54
+ # Tunable via env; AGENTKIT_HTTP_RETRIES=0 disables retries.
55
+ _RETRYABLE_STATUS = frozenset({429, 503})
56
+
57
+
58
+ def _http_timeout() -> float:
59
+ try:
60
+ return max(1.0, float(os.getenv("AGENTKIT_HTTP_TIMEOUT", "30")))
61
+ except ValueError:
62
+ return 30.0
63
+
64
+
65
+ def _http_retries() -> int:
66
+ try:
67
+ return max(0, int(os.getenv("AGENTKIT_HTTP_RETRIES", "2")))
68
+ except ValueError:
69
+ return 2
70
+
71
+
72
+ def _backoff_seconds(attempt: int) -> float:
73
+ return min(8.0, 0.5 * (2**attempt))
74
+
75
+
76
+ def _retry_after_seconds(resp: requests.Response) -> float | None:
77
+ raw = resp.headers.get("Retry-After")
78
+ if not raw:
79
+ return None
80
+ try:
81
+ return max(0.0, float(raw))
82
+ except ValueError:
83
+ return None
84
+
85
+
86
+ def _signed_request(method, url, headers, params, data) -> requests.Response:
87
+ """Issue a signed HTTP request with a bounded timeout and conservative
88
+ transient-retry.
89
+
90
+ Only retries failures that almost certainly mean the request was not
91
+ processed — connection errors (incl. connect timeouts) and HTTP 429/503 —
92
+ so non-idempotent ``Create*`` actions are never double-executed. Read
93
+ timeouts and other 5xx are surfaced, not retried. Honors ``Retry-After``.
94
+ """
95
+ retries = _http_retries()
96
+ timeout = _http_timeout()
97
+ resp: requests.Response | None = None
98
+ for attempt in range(retries + 1):
99
+ try:
100
+ resp = requests.request(
101
+ method=method,
102
+ url=url,
103
+ headers=headers,
104
+ params=params,
105
+ data=data,
106
+ timeout=timeout,
107
+ )
108
+ except requests.ConnectionError:
109
+ # Not delivered (connection failed/reset before completion), so a
110
+ # retry cannot double-execute the request.
111
+ if attempt < retries:
112
+ time.sleep(_backoff_seconds(attempt))
113
+ continue
114
+ raise
115
+ if resp.status_code in _RETRYABLE_STATUS and attempt < retries:
116
+ time.sleep(_retry_after_seconds(resp) or _backoff_seconds(attempt))
117
+ continue
118
+ return resp
119
+ assert resp is not None # loop always returns or raises before here
120
+ return resp
121
+
122
+
49
123
  def _get_os_tag() -> str:
50
124
  system = platform.system().lower()
51
125
  if "linux" in system:
@@ -221,7 +295,7 @@ def request(method, date, query, header, ak, sk, action, body):
221
295
  header = {**header, **sign_result}
222
296
  # header = {**header, **{"X-Security-Token": SessionToken}}
223
297
  # 第六步:将 Signature 签名写入 HTTP Header 中,并发送 HTTP 请求。
224
- r = requests.request(
298
+ r = _signed_request(
225
299
  method=method,
226
300
  url=f"{Scheme}://{request_param['host']}{request_param['path']}",
227
301
  headers=header,
agentkit/version.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- VERSION = "0.6.5"
15
+ VERSION = "0.7.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentkit-sdk-python
3
- Version: 0.6.5
3
+ Version: 0.7.1
4
4
  Summary: Python SDK for transforming any AI agent into a production-ready application. Framework-agnostic primitives for runtime, memory, authentication, and tools with volcengine-managed infrastructure.
5
5
  Author-email: Xiangrui Cheng <innsdcc@gmail.com>, Yumeng Bao <baoyumeng.123@gmail.com>, Yaozheng Fang <fangyozheng@gmail.com>, Guodong Li <cu.eric.lee@gmail.com>
6
6
  License: Apache License
@@ -1,5 +1,5 @@
1
1
  agentkit/__init__.py,sha256=l27ZMDslc3VhmmnPZJyrqVvTDoZ0LqhCtM5hw0caHcU,1021
2
- agentkit/version.py,sha256=-8u8ma3ZjT78npvdx0yb_H-M501lF15MhCY0uJyY6CU,653
2
+ agentkit/version.py,sha256=x8YYWLWsVZRn9Iz6baPWEWckXHw3be6Y347X-_UiPLU,653
3
3
  agentkit/apps/__init__.py,sha256=-oXTjxV3gejpJ8pffTUcUAXw0LfxKCYZJk-_hIJhtLM,1921
4
4
  agentkit/apps/base_app.py,sha256=3hZZExL1wyTGWveJEZZoqXN086MzmkVS_WU1vyulIWg,754
5
5
  agentkit/apps/utils.py,sha256=IzimIDmT6FS6PV9MLimPh46mq5zT-EjVmnYPxBdyEq0,1934
@@ -25,6 +25,7 @@ agentkit/auth/_sigv4.py,sha256=yAbFD57g3cBvCkGTVJap0BCakQeukyR_FlzMDYZdTkw,3375
25
25
  agentkit/auth/admin.py,sha256=U9rvVFx1rD5dsSrL0eQVPezBVI9vhaSrdc6QkM7jVKY,23393
26
26
  agentkit/auth/credential_hosting.py,sha256=lFFYseYHkqCUsv1djtK3EFR1XvESmw6l1Zgu1CcwIa8,24280
27
27
  agentkit/auth/errors.py,sha256=ZD5y1bxt_5oYs31dy3G1xRDHQSFVqxVaAxXdohg4948,1401
28
+ agentkit/auth/model_login.py,sha256=mdESwhfB5KotRaDuBf3ihcJckOMERDwDda1SySFUxsI,15387
28
29
  agentkit/auth/oauth.py,sha256=vbFRJpmlQXxcKRjHk2LIDLs1I3hsVrWTvLRnDA8XNN4,8812
29
30
  agentkit/auth/profile.py,sha256=gJpPj1ivpjRdH3MnjB2ICN4fYVaQlkt9DreJqkOze0Y,5656
30
31
  agentkit/auth/providers.py,sha256=zcOjEryJvuUctnRl3_Uae6HdZ8hGc0NVX4Lp3Y_2W68,3041
@@ -37,7 +38,7 @@ agentkit/auth/sts.py,sha256=mHcC4XJg9kwcX-Qt3_Ez88QnAOHkV7b_XRIIs3XznSM,5277
37
38
  agentkit/client/__init__.py,sha256=WHKgNGkpYsJp0xD20drYcxAMDclwDqrfURqxEVv0V1Y,1053
38
39
  agentkit/client/base_agentkit_client.py,sha256=OELc-y1aT-G2scAsS7p8gO4L4y9lA1ZqfDTLMt2Ajvc,2688
39
40
  agentkit/client/base_iam_client.py,sha256=F3ggcZjt14CAcgDYpncF2XGr1zTGnhaOzqEJl4NGHwU,2432
40
- agentkit/client/base_service_client.py,sha256=5PDivEpamjCkNDWZmkswqGEd7u1FjbFpLwPMeLXOy7k,11816
41
+ agentkit/client/base_service_client.py,sha256=lUb2aDFp9E-mCmCjOlCGkA0D_ziBuciiv-QEmKgK2-o,13234
41
42
  agentkit/platform/__init__.py,sha256=XD1YoXWJPpLGrP410Tb_psPFKR3LUo3-47UlqayUumo,3464
42
43
  agentkit/platform/configuration.py,sha256=LbnEaZT17FNuxa1yQWpG7ol3_D--EUj-AFgGddrmkzs,23347
43
44
  agentkit/platform/console_urls.py,sha256=JcnjwyuxN5c4RuXcPevsxy6aInurVFtchqbibJBqjSk,1125
@@ -81,7 +82,7 @@ agentkit/toolkit/builders/ve_pipeline.py,sha256=n2ZbjqkSBKv5oOsqD-E0SpC5NeP2Impr
81
82
  agentkit/toolkit/cli/__init__.py,sha256=pkSabKw7_ai4NOo56pXKL40EcaxIDh6HYxPXOY7qWbo,634
82
83
  agentkit/toolkit/cli/__main__.py,sha256=arwJ1gHkaaoQAXb0ezo_FeyxiMqVy0FXujB2WOX_ohU,775
83
84
  agentkit/toolkit/cli/cli.py,sha256=PFe6axB5f2Ox10azlf2Ditb1rXeVVMI5BbX46Cnu33k,4769
84
- agentkit/toolkit/cli/cli_add.py,sha256=1yP5zyErRJa4RfYKcI-OaPlyIaLHg62nHk87aEaKzPY,36505
85
+ agentkit/toolkit/cli/cli_add.py,sha256=aLyvN9Vcjis_KLhy92uuPi7e8ltFdHjwFOb-nME7CV0,39730
85
86
  agentkit/toolkit/cli/cli_auth.py,sha256=eislIHxafJovjDSQai8zfO4zcd502-U5oJYelnPFKgQ,26445
86
87
  agentkit/toolkit/cli/cli_build.py,sha256=gtRBPJfm6Z4y37ISC_CiGwEkMhD4fD7IQi1H6Lzc2Yo,2984
87
88
  agentkit/toolkit/cli/cli_config.py,sha256=cb-QGXNvm8kQiaii4vjfCwt6Y9oukiZFSgHp1Y8Ovuk,29096
@@ -89,7 +90,7 @@ agentkit/toolkit/cli/cli_delete.py,sha256=Q6utRf5ak1fHF_Bm73weWgiiQA3Oz3iWzl5ij5
89
90
  agentkit/toolkit/cli/cli_deploy.py,sha256=B912gRSpgLFBCLF8w8lD1J_jP5-PvpT_Zjblx-0yees,7066
90
91
  agentkit/toolkit/cli/cli_destroy.py,sha256=QpH7cctsaIFd2io6hhEeKnCjxK7DbE7AnFsecWL5BxY,1797
91
92
  agentkit/toolkit/cli/cli_init.py,sha256=feuypfgggXj6_0ZP4TEQ3rIZOuYRVUxXwvpdq9nsOY0,17911
92
- agentkit/toolkit/cli/cli_invoke.py,sha256=6fvL0yprSHHfj04vD2uA6AS27ouvpD33QIjgREZz01k,45204
93
+ agentkit/toolkit/cli/cli_invoke.py,sha256=sLaCXXoOeTPzECZFtNCV8EIlIQPQZsaih2s5rocz2h8,46082
93
94
  agentkit/toolkit/cli/cli_knowledge.py,sha256=J3pcJs01tWJAMp79nFnX5pU6AEZK7XVCZtyPRLKkfUI,25142
94
95
  agentkit/toolkit/cli/cli_launch.py,sha256=vkhjTBcxteVGwxtkDXKQxDjqTsFyDPx0SZcKL-zkPMg,3774
95
96
  agentkit/toolkit/cli/cli_list.py,sha256=KBqSRMaiO6Frej_ZvTV4_ODWgMVdHzJtsl63K43Y3hY,14256
@@ -106,11 +107,12 @@ agentkit/toolkit/cli/console_reporter.py,sha256=ta9GFUDkPEFfGeANDsza5bZdVXidqhHI
106
107
  agentkit/toolkit/cli/interactive_config.py,sha256=y5g5aNMJuX2tHUuu2pvacOssx3O3oqs0u_g0ZinyP1c,46133
107
108
  agentkit/toolkit/cli/utils.py,sha256=THEzN0Nln0sA3luIc9seP_oczBMDsHJAV6nlBhsWroI,8693
108
109
  agentkit/toolkit/cli/sandbox/__init__.py,sha256=8xnyvQUEVlMNQjoFf6drRqGL03Kdw8tVHEq2aMaUK2Y,1351
109
- agentkit/toolkit/cli/sandbox/cli.py,sha256=73obe5G-LOa1b7p5lwMgIy19pta7FZSS9m_fTDmb-tY,1863
110
+ agentkit/toolkit/cli/sandbox/cli.py,sha256=UukUNxsDmyQ66QQjM-a8W0l6BuVqgb1bbesEU3P3RD4,2089
110
111
  agentkit/toolkit/cli/sandbox/cli_create.py,sha256=_3yPBnfJ9igDbQAkeag7oksWEw2Hz-s0GftzKD6jSKc,14403
111
112
  agentkit/toolkit/cli/sandbox/cli_exec.py,sha256=m70Po9ybYL_ZAskpceNamQgtg3QvlNoAjyQPqHOdbb0,17479
112
113
  agentkit/toolkit/cli/sandbox/cli_file.py,sha256=AXNV0qPlWY_GS6InKkVTQ9cMSSkdiOIxC2dfAHED-Fk,28715
113
114
  agentkit/toolkit/cli/sandbox/cli_get.py,sha256=sbdCXIzR6L3vUHZvwaC19GWbOJQixR37zDONIRdNhzo,3218
115
+ agentkit/toolkit/cli/sandbox/cli_model_login.py,sha256=Y-pBu0onNdZ9lPP0rKQT6ZG3PEmeILzIl4-wbcbwtLA,9351
114
116
  agentkit/toolkit/cli/sandbox/cli_mount.py,sha256=ZKjEOifPvRHhqdMXlTMqc1ZtW0TSjdfJuJ0yzl6F0PY,11215
115
117
  agentkit/toolkit/cli/sandbox/cli_run.py,sha256=7TTRssT1CNGIC-8JRGtPazRJZDX_9WM-MhIgVR_sZ-g,11414
116
118
  agentkit/toolkit/cli/sandbox/cli_shell.py,sha256=PniGXNmw4PoZRiB0P29jryra_aFtpuCwJgv5vlxfFeU,4642
@@ -191,7 +193,7 @@ agentkit/toolkit/resources/wrappers/wrapper_stream.py.jinja2,sha256=V5KeODA426nN
191
193
  agentkit/toolkit/runners/__init__.py,sha256=c-PuCvGFpIQrYC_W9qp2ZztgTSH2AVgxSEgkXMqJKZM,1829
192
194
  agentkit/toolkit/runners/base.py,sha256=GosKJUUVa1V85y9bhoT8SZthp5sV7KEfvn9gzL1MkoQ,29426
193
195
  agentkit/toolkit/runners/local_docker.py,sha256=YQTFcxB_PxIx_7SXleODBBBRCY9GuAgpkYQLCm8M8KA,28825
194
- agentkit/toolkit/runners/ve_agentkit.py,sha256=xf9UXMLIHZ-hVo3GD6v2PnjBuCH0g07ZTMBaI3grHKk,55206
196
+ agentkit/toolkit/runners/ve_agentkit.py,sha256=jIhb9ycgZMJxxxw9XKKyC5U2ZwTV-ArGsaJsQ4VHOWM,55507
195
197
  agentkit/toolkit/sdk/__init__.py,sha256=SK_ElW74IiT2WRhDIJhL0xhpAk4C8xHI7ZUCnGgW2hs,2735
196
198
  agentkit/toolkit/sdk/builder.py,sha256=tI-uawiJ_BYVH5FbDxVWJIXC-56rM4eUzX2o7M4pkMI,4225
197
199
  agentkit/toolkit/sdk/client.py,sha256=EbGhgcWgc50Z39AeXnvtip2r5fH_qx9YhM2CXOwlR_8,18136
@@ -227,10 +229,10 @@ agentkit/utils/logging_config.py,sha256=419kWZ1YWAoejdSRm71XOWjCZ9aghhelweL8bj2n
227
229
  agentkit/utils/misc.py,sha256=FXMlcupj3KzxfJvMYFp0uTQk3FRMx8FiEXkYiy-UQ8w,3437
228
230
  agentkit/utils/request.py,sha256=IVoat3EavR9rQ_fXi0eA2rZPCJ9lVZAXXn7uIIX6bY4,1614
229
231
  agentkit/utils/template_utils.py,sha256=Qjg9V6dEpjAd_yYezIIPwuDsDeeMmp6XTMn5ZECifNc,6336
230
- agentkit/utils/ve_sign.py,sha256=nrYiS2bYncvWKQVpVMzwYwPDfkA0OJS7LO9l_wfW0xc,9310
231
- agentkit_sdk_python-0.6.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
232
- agentkit_sdk_python-0.6.5.dist-info/METADATA,sha256=-cvLyYEu2KfEUXw6UkEhVrbRr7_Ud-IiQYTurDxz3q4,19462
233
- agentkit_sdk_python-0.6.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
234
- agentkit_sdk_python-0.6.5.dist-info/entry_points.txt,sha256=fhzZUsvsLXeB4mPaa0SBQiTBqFT404uDAKPaePAOUAE,58
235
- agentkit_sdk_python-0.6.5.dist-info/top_level.txt,sha256=ipy8JF-QQ-V0C1oRFLxsyaW8zwrasfJ-zjAh9vgOc7U,9
236
- agentkit_sdk_python-0.6.5.dist-info/RECORD,,
232
+ agentkit/utils/ve_sign.py,sha256=Vk_ku0t2665CaR2iPndoNWudifVa0cYnpl4ZXqEojBY,11832
233
+ agentkit_sdk_python-0.7.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
234
+ agentkit_sdk_python-0.7.1.dist-info/METADATA,sha256=1r6tYfTNDy2zpM5UC8ZJ3iI9NuZ6VwJ3-EXr9MkSg2I,19462
235
+ agentkit_sdk_python-0.7.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
236
+ agentkit_sdk_python-0.7.1.dist-info/entry_points.txt,sha256=fhzZUsvsLXeB4mPaa0SBQiTBqFT404uDAKPaePAOUAE,58
237
+ agentkit_sdk_python-0.7.1.dist-info/top_level.txt,sha256=ipy8JF-QQ-V0C1oRFLxsyaW8zwrasfJ-zjAh9vgOc7U,9
238
+ agentkit_sdk_python-0.7.1.dist-info/RECORD,,