agentkit-sdk-python 0.6.2__py3-none-any.whl → 0.6.4__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.
- agentkit/auth/session.py +95 -0
- agentkit/auth/sso.py +9 -1
- agentkit/platform/configuration.py +13 -6
- agentkit/sdk/skills/client.py +12 -0
- agentkit/sdk/skills/types.py +21 -1
- agentkit/toolkit/cli/cli.py +4 -0
- agentkit/toolkit/cli/cli_delete.py +102 -0
- agentkit/toolkit/cli/cli_deploy.py +52 -11
- agentkit/toolkit/cli/cli_invoke.py +250 -4
- agentkit/toolkit/cli/cli_list.py +201 -0
- agentkit/toolkit/cli/cli_logs.py +253 -0
- agentkit/toolkit/cli/cli_skill.py +476 -0
- agentkit/toolkit/cli/sandbox/__init__.py +2 -0
- agentkit/toolkit/cli/sandbox/cli.py +4 -0
- agentkit/toolkit/cli/sandbox/cli_create.py +77 -74
- agentkit/toolkit/cli/sandbox/cli_exec.py +133 -14
- agentkit/toolkit/cli/sandbox/cli_file.py +4 -2
- agentkit/toolkit/cli/sandbox/cli_get.py +1 -1
- agentkit/toolkit/cli/sandbox/cli_mount.py +361 -0
- agentkit/toolkit/cli/sandbox/cli_run.py +365 -0
- agentkit/toolkit/cli/sandbox/cli_shell.py +23 -9
- agentkit/toolkit/cli/sandbox/cli_web.py +11 -19
- agentkit/toolkit/cli/sandbox/git_config.py +189 -0
- agentkit/toolkit/cli/sandbox/model_config.py +218 -42
- agentkit/toolkit/cli/sandbox/session_create.py +72 -60
- agentkit/toolkit/cli/sandbox/session_sync.py +1 -1
- agentkit/toolkit/cli/sandbox/tool_resolve.py +68 -4
- agentkit/toolkit/cli/sandbox/tos_config.py +141 -0
- agentkit/toolkit/config/utils.py +17 -0
- agentkit/toolkit/executors/init_executor.py +13 -3
- agentkit/toolkit/harness/__init__.py +2 -1
- agentkit/toolkit/harness/config_builder.py +7 -1
- agentkit/toolkit/harness/deploy.py +146 -5
- agentkit/toolkit/sdk/__init__.py +2 -1
- agentkit/toolkit/volcengine/apmplus_logs.py +184 -0
- agentkit/version.py +1 -1
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/METADATA +2 -1
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/RECORD +43 -36
- /agentkit/toolkit/cli/sandbox/{utils.py → sandbox_client.py} +0 -0
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/WHEEL +0 -0
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/entry_points.txt +0 -0
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {agentkit_sdk_python-0.6.2.dist-info → agentkit_sdk_python-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any
|
|
19
|
+
import base64
|
|
20
|
+
import binascii
|
|
19
21
|
import json
|
|
20
22
|
import typer
|
|
21
23
|
from typer.core import TyperGroup
|
|
@@ -627,6 +629,160 @@ def build_harness_overrides(
|
|
|
627
629
|
return overrides
|
|
628
630
|
|
|
629
631
|
|
|
632
|
+
# Fixed ADK app name for the run_sse path. The harness loader serves its single
|
|
633
|
+
# agent under any app name, so a stable constant keeps the CLI decoupled from the
|
|
634
|
+
# deployed HARNESS_NAME.
|
|
635
|
+
_HARNESS_RUN_SSE_APP = "harness"
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _user_id_from_token(token: str) -> str | None:
|
|
639
|
+
"""Return the OIDC ``sub`` claim from a JWT bearer token, else ``None``.
|
|
640
|
+
|
|
641
|
+
A custom_jwt harness is called with an OIDC id_token whose ``sub`` is the
|
|
642
|
+
authenticated user's stable id — use it as the run's user_id so sessions are
|
|
643
|
+
tied to the real identity. A key_auth token is an opaque api key (not a JWT,
|
|
644
|
+
no ``sub``), so this returns ``None`` and the caller falls back to a random id.
|
|
645
|
+
"""
|
|
646
|
+
parts = token.split(".")
|
|
647
|
+
if len(parts) != 3:
|
|
648
|
+
return None # not a JWT (e.g. a key_auth api key)
|
|
649
|
+
payload = parts[1]
|
|
650
|
+
payload += "=" * (-len(payload) % 4) # restore base64 padding
|
|
651
|
+
try:
|
|
652
|
+
claims = json.loads(base64.urlsafe_b64decode(payload))
|
|
653
|
+
except (ValueError, binascii.Error):
|
|
654
|
+
return None # malformed JWT payload → fall back to random
|
|
655
|
+
sub = claims.get("sub")
|
|
656
|
+
return sub if isinstance(sub, str) and sub else None
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _harness_run_sse(
|
|
660
|
+
*,
|
|
661
|
+
base_url: str,
|
|
662
|
+
token: str,
|
|
663
|
+
prompt: str,
|
|
664
|
+
session_id: str,
|
|
665
|
+
overrides: dict,
|
|
666
|
+
raw: bool,
|
|
667
|
+
) -> Any:
|
|
668
|
+
"""Invoke a deployed harness via the ADK ``/run_sse`` endpoint (streaming).
|
|
669
|
+
|
|
670
|
+
app_name is the fixed ``"harness"``; user_id is the JWT ``sub`` when the token
|
|
671
|
+
is an OIDC id_token, else a random id; session_id is the caller's. When
|
|
672
|
+
``overrides`` is non-empty it is sent as the ``harness`` field so the runtime
|
|
673
|
+
streams a spawned (overridden) agent; otherwise the base agent.
|
|
674
|
+
"""
|
|
675
|
+
import requests
|
|
676
|
+
|
|
677
|
+
app_name = _HARNESS_RUN_SSE_APP
|
|
678
|
+
sub = _user_id_from_token(token)
|
|
679
|
+
user_id = sub or f"u-{uuid.uuid4().hex[:12]}"
|
|
680
|
+
user_id_origin = "jwt sub" if sub else "random"
|
|
681
|
+
headers = {"Content-Type": "application/json"}
|
|
682
|
+
if token:
|
|
683
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
684
|
+
console.print(
|
|
685
|
+
f"[blue]run_sse: app_name={app_name}, user_id={user_id} ({user_id_origin}), "
|
|
686
|
+
f"session_id={session_id}[/blue]"
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
# ADK /run_sse requires an existing session; create it (ignore "exists").
|
|
690
|
+
session_url = f"{base_url}/apps/{app_name}/users/{user_id}/sessions/{session_id}"
|
|
691
|
+
try:
|
|
692
|
+
sr = requests.post(session_url, json={}, headers=headers, timeout=60)
|
|
693
|
+
except requests.RequestException as e:
|
|
694
|
+
console.print(f"[red]❌ Session create failed: {e}[/red]")
|
|
695
|
+
raise typer.Exit(1)
|
|
696
|
+
if sr.status_code not in (200, 400, 409):
|
|
697
|
+
console.print(
|
|
698
|
+
f"[red]❌ Session create HTTP {sr.status_code}: {sr.text[:300]}[/red]"
|
|
699
|
+
)
|
|
700
|
+
raise typer.Exit(1)
|
|
701
|
+
|
|
702
|
+
body: dict[str, Any] = {
|
|
703
|
+
"app_name": app_name,
|
|
704
|
+
"user_id": user_id,
|
|
705
|
+
"session_id": session_id,
|
|
706
|
+
"new_message": {"role": "user", "parts": [{"text": prompt}]},
|
|
707
|
+
"streaming": True,
|
|
708
|
+
}
|
|
709
|
+
if overrides:
|
|
710
|
+
body["harness"] = overrides
|
|
711
|
+
console.print(f"[blue]Using one-time overrides: {overrides}[/blue]")
|
|
712
|
+
try:
|
|
713
|
+
resp = requests.post(
|
|
714
|
+
f"{base_url}/run_sse",
|
|
715
|
+
json=body,
|
|
716
|
+
headers=headers,
|
|
717
|
+
timeout=300,
|
|
718
|
+
stream=True,
|
|
719
|
+
)
|
|
720
|
+
except requests.RequestException as e:
|
|
721
|
+
console.print(f"[red]❌ run_sse request failed: {e}[/red]")
|
|
722
|
+
raise typer.Exit(1)
|
|
723
|
+
if resp.status_code != 200:
|
|
724
|
+
console.print(
|
|
725
|
+
f"[red]❌ run_sse HTTP {resp.status_code}: {resp.text[:500]}[/red]"
|
|
726
|
+
)
|
|
727
|
+
raise typer.Exit(1)
|
|
728
|
+
|
|
729
|
+
def _answer_text(event: dict) -> str:
|
|
730
|
+
# Only the answer text; the model's "thought" (reasoning) parts stay hidden
|
|
731
|
+
# behind the thinking spinner.
|
|
732
|
+
parts = (event.get("content") or {}).get("parts") or []
|
|
733
|
+
return "".join(
|
|
734
|
+
p["text"]
|
|
735
|
+
for p in parts
|
|
736
|
+
if isinstance(p, dict) and p.get("text") and not p.get("thought")
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
streamed = []
|
|
740
|
+
final_answer = ""
|
|
741
|
+
answer_open = False # first answer token seen → spinner stopped, reply started
|
|
742
|
+
# A spinner covers the wait (model latency + reasoning) until the answer starts.
|
|
743
|
+
status = None if raw else console.status("thinking", spinner="dots")
|
|
744
|
+
if status is not None:
|
|
745
|
+
status.start()
|
|
746
|
+
try:
|
|
747
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
748
|
+
if not line:
|
|
749
|
+
continue
|
|
750
|
+
if raw:
|
|
751
|
+
print(line) # builtin print: no rich wrapping/markup on raw JSON
|
|
752
|
+
continue
|
|
753
|
+
event = _normalize_stream_event(line)
|
|
754
|
+
if not isinstance(event, dict):
|
|
755
|
+
continue
|
|
756
|
+
if event.get("error"):
|
|
757
|
+
if status is not None and not answer_open:
|
|
758
|
+
status.stop()
|
|
759
|
+
console.print(f"\n[red]Error: {event['error']}[/red]")
|
|
760
|
+
continue
|
|
761
|
+
answer = _answer_text(event)
|
|
762
|
+
# `partial=False` is the final aggregate (repeats everything) — keep it
|
|
763
|
+
# as a fallback but don't print it; partial events stream the deltas.
|
|
764
|
+
if event.get("partial") is False:
|
|
765
|
+
final_answer = answer or final_answer
|
|
766
|
+
continue
|
|
767
|
+
if answer:
|
|
768
|
+
if not answer_open:
|
|
769
|
+
if status is not None:
|
|
770
|
+
status.stop() # leave the thinking phase
|
|
771
|
+
console.print("") # blank line before the reply
|
|
772
|
+
answer_open = True
|
|
773
|
+
console.print(answer, end="", style="green")
|
|
774
|
+
streamed.append(answer)
|
|
775
|
+
finally:
|
|
776
|
+
if status is not None and not answer_open:
|
|
777
|
+
status.stop()
|
|
778
|
+
|
|
779
|
+
if not streamed and final_answer:
|
|
780
|
+
console.print("")
|
|
781
|
+
console.print(final_answer, end="", style="green")
|
|
782
|
+
console.print("")
|
|
783
|
+
return "".join(streamed) or final_answer
|
|
784
|
+
|
|
785
|
+
|
|
630
786
|
@invoke_app.command("harness")
|
|
631
787
|
def harness_command(
|
|
632
788
|
name: str = typer.Argument(
|
|
@@ -640,7 +796,9 @@ def harness_command(
|
|
|
640
796
|
"agentkit_user", "--user-id", help="user_id for the run."
|
|
641
797
|
),
|
|
642
798
|
session_id: str = typer.Option(
|
|
643
|
-
|
|
799
|
+
None,
|
|
800
|
+
"--session-id",
|
|
801
|
+
help="session_id for the run (random if unset).",
|
|
644
802
|
),
|
|
645
803
|
max_llm_calls: int = typer.Option(
|
|
646
804
|
None,
|
|
@@ -675,7 +833,12 @@ def harness_command(
|
|
|
675
833
|
help="Bearer token override (e.g. OAuth JWT for custom_jwt harnesses).",
|
|
676
834
|
),
|
|
677
835
|
raw: bool = typer.Option(
|
|
678
|
-
False, "--raw", help="Print the raw InvokeHarnessResponse
|
|
836
|
+
False, "--raw", help="Print the raw response (InvokeHarnessResponse / SSE)."
|
|
837
|
+
),
|
|
838
|
+
protocol: str = typer.Option(
|
|
839
|
+
"run_sse",
|
|
840
|
+
"--protocol",
|
|
841
|
+
help="Transport: 'run_sse' (ADK /run_sse, default) or 'invoke' (POST /harness/invoke).",
|
|
679
842
|
),
|
|
680
843
|
) -> Any:
|
|
681
844
|
"""Invoke a deployed harness by name (resolved via the harness.json registry).
|
|
@@ -712,8 +875,69 @@ def harness_command(
|
|
|
712
875
|
)
|
|
713
876
|
raise typer.Exit(1)
|
|
714
877
|
|
|
715
|
-
|
|
716
|
-
|
|
878
|
+
if protocol not in ("invoke", "run_sse"):
|
|
879
|
+
console.print(
|
|
880
|
+
f"[red]Error: --protocol must be 'invoke' or 'run_sse', got '{protocol}'.[/red]"
|
|
881
|
+
)
|
|
882
|
+
raise typer.Exit(1)
|
|
883
|
+
|
|
884
|
+
base_url = entry["url"].rstrip("/")
|
|
885
|
+
|
|
886
|
+
# Inbound credential selection is auth-type aware — the registry records how each
|
|
887
|
+
# harness was deployed (harness/deploy.py _record_harness): a custom_jwt harness is
|
|
888
|
+
# {auth_type:"custom_jwt", no "key"}; a key_auth harness is {"key": ...}.
|
|
889
|
+
# 1. --apikey always wins (explicit manual override).
|
|
890
|
+
# 2. custom_jwt harness → the `agentkit login` id_token (OIDC JWT, auto-refreshed) —
|
|
891
|
+
# the data plane's JWT path; refresh failure errors (re-login), never silent.
|
|
892
|
+
# 3. key_auth harness → the static "key"; a key_auth authorizer would reject a JWT,
|
|
893
|
+
# so we never force the id_token onto it.
|
|
894
|
+
# Both transports (invoke and run_sse) share this resolution.
|
|
895
|
+
from agentkit.auth.errors import AuthError
|
|
896
|
+
from agentkit.auth.sso import load_session
|
|
897
|
+
|
|
898
|
+
is_jwt_harness = entry.get("auth_type") == "custom_jwt" or not entry.get("key")
|
|
899
|
+
auth_session = None # set only when the login id_token is the credential (enables 401 refresh)
|
|
900
|
+
token = apikey or ""
|
|
901
|
+
if not token and is_jwt_harness:
|
|
902
|
+
try:
|
|
903
|
+
auth_session = load_session()
|
|
904
|
+
except Exception:
|
|
905
|
+
auth_session = None
|
|
906
|
+
if auth_session is not None:
|
|
907
|
+
try:
|
|
908
|
+
token = auth_session.valid_id_token()
|
|
909
|
+
except AuthError as e:
|
|
910
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
911
|
+
if getattr(e, "hint", None):
|
|
912
|
+
console.print(f"[yellow]{e.hint}[/yellow]")
|
|
913
|
+
raise typer.Exit(1)
|
|
914
|
+
if not token:
|
|
915
|
+
token = entry.get("key") or ""
|
|
916
|
+
|
|
917
|
+
# No session given → mint a random one (both transports behave identically;
|
|
918
|
+
# creating it is idempotent).
|
|
919
|
+
session_id = session_id or f"s-{uuid.uuid4().hex[:12]}"
|
|
920
|
+
|
|
921
|
+
if protocol == "run_sse":
|
|
922
|
+
# run_sse supports the same overrides (sent as the `harness` field); only
|
|
923
|
+
# --max-llm-calls is invoke-only (not part of the ADK run_sse request).
|
|
924
|
+
if max_llm_calls is not None:
|
|
925
|
+
console.print(
|
|
926
|
+
"[yellow]Note: --max-llm-calls is ignored with --protocol "
|
|
927
|
+
"run_sse.[/yellow]"
|
|
928
|
+
)
|
|
929
|
+
return _harness_run_sse(
|
|
930
|
+
base_url=base_url,
|
|
931
|
+
token=token,
|
|
932
|
+
prompt=message,
|
|
933
|
+
session_id=session_id,
|
|
934
|
+
overrides=build_harness_overrides(
|
|
935
|
+
system_prompt, model_name, tools, skills, runtime
|
|
936
|
+
),
|
|
937
|
+
raw=raw,
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
invoke_url = base_url + "/harness/invoke"
|
|
717
941
|
req_headers = {"Content-Type": "application/json"}
|
|
718
942
|
if token:
|
|
719
943
|
req_headers["Authorization"] = f"Bearer {token}"
|
|
@@ -739,6 +963,18 @@ def harness_command(
|
|
|
739
963
|
|
|
740
964
|
try:
|
|
741
965
|
resp = requests.post(invoke_url, json=body, headers=req_headers, timeout=300)
|
|
966
|
+
# If the harness rejects a locally-valid JWT (clock skew, mid-flight rotation,
|
|
967
|
+
# server-side revocation), force one refresh and retry exactly once.
|
|
968
|
+
if resp.status_code == 401 and auth_session is not None and not apikey:
|
|
969
|
+
try:
|
|
970
|
+
token = auth_session.valid_id_token(force_refresh=True)
|
|
971
|
+
except AuthError as e:
|
|
972
|
+
console.print(f"[red]❌ session refresh failed: {e}[/red]")
|
|
973
|
+
if getattr(e, "hint", None):
|
|
974
|
+
console.print(f"[yellow]{e.hint}[/yellow]")
|
|
975
|
+
raise typer.Exit(1)
|
|
976
|
+
req_headers["Authorization"] = f"Bearer {token}"
|
|
977
|
+
resp = requests.post(invoke_url, json=body, headers=req_headers, timeout=300)
|
|
742
978
|
except requests.RequestException as e:
|
|
743
979
|
console.print(f"[red]❌ Request to {invoke_url} failed: {e}[/red]")
|
|
744
980
|
raise typer.Exit(1)
|
|
@@ -755,10 +991,20 @@ def harness_command(
|
|
|
755
991
|
console.print(f"[red]❌ Non-JSON response: {resp.text}[/red]")
|
|
756
992
|
raise typer.Exit(1)
|
|
757
993
|
|
|
994
|
+
# The harness returns failures (unsupported tool, skill load failure, or a
|
|
995
|
+
# runtime error) in `error`; surface it verbatim instead of as normal output.
|
|
996
|
+
error = data.get("error") if isinstance(data, dict) else None
|
|
997
|
+
|
|
758
998
|
if raw:
|
|
759
999
|
console.print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
1000
|
+
if error:
|
|
1001
|
+
raise typer.Exit(1)
|
|
760
1002
|
return str(data)
|
|
761
1003
|
|
|
1004
|
+
if error:
|
|
1005
|
+
console.print(f"[red]❌ Harness error: {error}[/red]")
|
|
1006
|
+
raise typer.Exit(1)
|
|
1007
|
+
|
|
762
1008
|
console.print("[green]✅ Invocation successful[/green]")
|
|
763
1009
|
if data.get("overwrite"):
|
|
764
1010
|
console.print("[yellow](served with one-time overrides)[/yellow]")
|
agentkit/toolkit/cli/cli_list.py
CHANGED
|
@@ -46,6 +46,39 @@ def _is_harness_runtime(runtime: rt.AgentKitRuntimesForListRuntimes) -> bool:
|
|
|
46
46
|
return False
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# Fixed ADK app name the harness loader serves its agent under (the deployed
|
|
50
|
+
# HARNESS_NAME is irrelevant to the ADK app path).
|
|
51
|
+
_HARNESS_APP = "harness"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _user_id_from_token(token: str) -> Optional[str]:
|
|
55
|
+
"""Return the OIDC ``sub`` claim from a JWT bearer token, else ``None``.
|
|
56
|
+
|
|
57
|
+
A custom_jwt harness is reached with an OIDC id_token whose ``sub`` is the
|
|
58
|
+
authenticated user's stable id. A key_auth token is an opaque api key (not a
|
|
59
|
+
JWT, no ``sub``), so this returns ``None`` and the caller must be given an
|
|
60
|
+
explicit ``--user-id``.
|
|
61
|
+
|
|
62
|
+
NOTE: this mirrors ``cli_invoke._user_id_from_token``; consolidate the two
|
|
63
|
+
into one shared helper once the ``invoke harness`` run_sse work lands.
|
|
64
|
+
"""
|
|
65
|
+
import base64
|
|
66
|
+
import binascii
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
parts = token.split(".")
|
|
70
|
+
if len(parts) != 3:
|
|
71
|
+
return None # not a JWT (e.g. a key_auth api key)
|
|
72
|
+
payload = parts[1]
|
|
73
|
+
payload += "=" * (-len(payload) % 4) # restore base64 padding
|
|
74
|
+
try:
|
|
75
|
+
claims = json.loads(base64.urlsafe_b64decode(payload))
|
|
76
|
+
except (ValueError, binascii.Error):
|
|
77
|
+
return None # malformed JWT payload
|
|
78
|
+
sub = claims.get("sub")
|
|
79
|
+
return sub if isinstance(sub, str) and sub else None
|
|
80
|
+
|
|
81
|
+
|
|
49
82
|
@list_app.command("harness")
|
|
50
83
|
def list_harness_command(
|
|
51
84
|
region: Optional[str] = typer.Option(
|
|
@@ -117,6 +150,7 @@ def list_harness_command(
|
|
|
117
150
|
("RuntimeId", "RuntimeId", "cyan"),
|
|
118
151
|
("Name", "Name", "white"),
|
|
119
152
|
("Status", "Status", "green"),
|
|
153
|
+
("CurrentVersionNumber", "Version", "blue"),
|
|
120
154
|
("ProjectName", "ProjectName", "yellow"),
|
|
121
155
|
("UpdatedAt", "UpdatedAt", "magenta"),
|
|
122
156
|
]
|
|
@@ -128,6 +162,173 @@ def list_harness_command(
|
|
|
128
162
|
local_console.print(table)
|
|
129
163
|
|
|
130
164
|
|
|
165
|
+
def _resolve_harness_endpoint(harness: str, directory: str, apikey: Optional[str]):
|
|
166
|
+
"""Resolve a deployed harness's data-plane base URL and bearer token.
|
|
167
|
+
|
|
168
|
+
Mirrors ``invoke harness``'s auth-type-aware credential selection:
|
|
169
|
+
1. ``--apikey`` always wins (explicit override);
|
|
170
|
+
2. a custom_jwt harness (no stored ``key``) → the ``agentkit login`` OIDC
|
|
171
|
+
id_token (auto-refreshed), whose ``sub`` identifies the caller;
|
|
172
|
+
3. a key_auth harness → its static ``key`` (an opaque api key, not a JWT).
|
|
173
|
+
|
|
174
|
+
Returns ``(base_url, token)``. Fast-fails when the harness is not in the
|
|
175
|
+
registry, or when a needed login id_token cannot be refreshed.
|
|
176
|
+
"""
|
|
177
|
+
from agentkit.toolkit.harness import load_harness_registry
|
|
178
|
+
|
|
179
|
+
registry = load_harness_registry(directory)
|
|
180
|
+
entry = registry.get(harness)
|
|
181
|
+
if not isinstance(entry, dict) or not entry.get("url"):
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
registry_path = Path(directory).resolve() / "harness.json"
|
|
185
|
+
console.print(
|
|
186
|
+
f"[red]Error: harness '{harness}' not found in registry {registry_path}. "
|
|
187
|
+
f"Deploy it first with `agentkit deploy --harness {harness}`.[/red]"
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
base_url = entry["url"].rstrip("/")
|
|
192
|
+
|
|
193
|
+
from agentkit.auth.errors import AuthError
|
|
194
|
+
from agentkit.auth.sso import load_session
|
|
195
|
+
|
|
196
|
+
is_jwt_harness = entry.get("auth_type") == "custom_jwt" or not entry.get("key")
|
|
197
|
+
token = apikey or ""
|
|
198
|
+
if not token and is_jwt_harness:
|
|
199
|
+
try:
|
|
200
|
+
auth_session = load_session()
|
|
201
|
+
except Exception:
|
|
202
|
+
auth_session = None
|
|
203
|
+
if auth_session is not None:
|
|
204
|
+
try:
|
|
205
|
+
token = auth_session.valid_id_token()
|
|
206
|
+
except AuthError as e:
|
|
207
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
208
|
+
if getattr(e, "hint", None):
|
|
209
|
+
console.print(f"[yellow]{e.hint}[/yellow]")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
if not token:
|
|
212
|
+
token = entry.get("key") or ""
|
|
213
|
+
|
|
214
|
+
return base_url, token
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@list_app.command("sessions")
|
|
218
|
+
def list_sessions_command(
|
|
219
|
+
harness: str = typer.Option(
|
|
220
|
+
..., "--harness", help="Harness name (resolved via the harness.json registry)."
|
|
221
|
+
),
|
|
222
|
+
user_id: Optional[str] = typer.Option(
|
|
223
|
+
None,
|
|
224
|
+
"--user-id",
|
|
225
|
+
help=(
|
|
226
|
+
"user_id whose sessions to list. If omitted, it is taken from the "
|
|
227
|
+
"JWT `sub` of the harness credential; a key_auth harness has no JWT, "
|
|
228
|
+
"so --user-id is required there."
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
apikey: Optional[str] = typer.Option(
|
|
232
|
+
None,
|
|
233
|
+
"--apikey",
|
|
234
|
+
"-ak",
|
|
235
|
+
help="Bearer token (e.g. a custom_jwt harness's OIDC JWT) overriding the registry credential.",
|
|
236
|
+
),
|
|
237
|
+
directory: str = typer.Option(
|
|
238
|
+
".", "--directory", help="Directory containing harness.json."
|
|
239
|
+
),
|
|
240
|
+
output: str = typer.Option(
|
|
241
|
+
"table", "--output", help="Output format: table|json|yaml"
|
|
242
|
+
),
|
|
243
|
+
quiet: bool = typer.Option(
|
|
244
|
+
False, "--quiet", "-q", help="Print only session ids"
|
|
245
|
+
),
|
|
246
|
+
no_color: bool = typer.Option(
|
|
247
|
+
False, "--no-color", "-nc", help="Disable colored output for tables/panels"
|
|
248
|
+
),
|
|
249
|
+
):
|
|
250
|
+
"""List a user's conversation sessions on a deployed harness runtime.
|
|
251
|
+
|
|
252
|
+
Calls the harness runtime's ADK endpoint
|
|
253
|
+
``GET /apps/harness/users/{user_id}/sessions``. ADK requires a user_id (there
|
|
254
|
+
is no cross-user listing), so it must be given explicitly via --user-id or be
|
|
255
|
+
derivable from the JWT `sub` of the harness credential.
|
|
256
|
+
"""
|
|
257
|
+
import json
|
|
258
|
+
from datetime import datetime
|
|
259
|
+
|
|
260
|
+
import requests
|
|
261
|
+
from rich.table import Table
|
|
262
|
+
|
|
263
|
+
local_console = console if not no_color else Console(no_color=True)
|
|
264
|
+
|
|
265
|
+
base_url, token = _resolve_harness_endpoint(harness, directory, apikey)
|
|
266
|
+
|
|
267
|
+
resolved_user_id = user_id or _user_id_from_token(token)
|
|
268
|
+
if not resolved_user_id:
|
|
269
|
+
local_console.print(
|
|
270
|
+
"[red]Error: cannot determine user_id — no --user-id given and the "
|
|
271
|
+
"harness credential is not a JWT with a `sub` claim (e.g. a key_auth "
|
|
272
|
+
"harness). Pass --user-id explicitly.[/red]"
|
|
273
|
+
)
|
|
274
|
+
raise typer.Exit(1)
|
|
275
|
+
|
|
276
|
+
url = f"{base_url}/apps/{_HARNESS_APP}/users/{resolved_user_id}/sessions"
|
|
277
|
+
headers = {}
|
|
278
|
+
if token:
|
|
279
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
280
|
+
|
|
281
|
+
with local_console.status("[cyan]Fetching sessions...[/cyan]", spinner="dots"):
|
|
282
|
+
try:
|
|
283
|
+
resp = requests.get(url, headers=headers, timeout=60)
|
|
284
|
+
except requests.RequestException as e:
|
|
285
|
+
local_console.print(f"[red]❌ List sessions request failed: {e}[/red]")
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
if resp.status_code != 200:
|
|
289
|
+
local_console.print(
|
|
290
|
+
f"[red]❌ List sessions HTTP {resp.status_code}: {resp.text[:300]}[/red]"
|
|
291
|
+
)
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
|
|
294
|
+
sessions = resp.json() or []
|
|
295
|
+
|
|
296
|
+
if quiet:
|
|
297
|
+
for s in sessions:
|
|
298
|
+
local_console.print(s.get("id", ""))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if output.lower() == "json":
|
|
302
|
+
local_console.print(json.dumps(sessions, indent=2, ensure_ascii=False))
|
|
303
|
+
return
|
|
304
|
+
if output.lower() == "yaml":
|
|
305
|
+
import yaml
|
|
306
|
+
|
|
307
|
+
local_console.print(yaml.safe_dump(sessions, sort_keys=False, allow_unicode=True))
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
table = Table(
|
|
311
|
+
title=f"Sessions for user '{resolved_user_id}' (Count: {len(sessions)})",
|
|
312
|
+
show_lines=False,
|
|
313
|
+
)
|
|
314
|
+
table.add_column("SessionId", style="cyan")
|
|
315
|
+
table.add_column("Events", style="green")
|
|
316
|
+
table.add_column("LastUpdate", style="magenta")
|
|
317
|
+
for s in sessions:
|
|
318
|
+
ts = s.get("lastUpdateTime")
|
|
319
|
+
last_update = (
|
|
320
|
+
datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
|
321
|
+
if isinstance(ts, (int, float)) and ts
|
|
322
|
+
else ""
|
|
323
|
+
)
|
|
324
|
+
table.add_row(
|
|
325
|
+
str(s.get("id", "")),
|
|
326
|
+
str(len(s.get("events", []))),
|
|
327
|
+
last_update,
|
|
328
|
+
)
|
|
329
|
+
local_console.print(table)
|
|
330
|
+
|
|
331
|
+
|
|
131
332
|
@list_app.command("credentials")
|
|
132
333
|
def list_credentials_command(
|
|
133
334
|
region: Optional[str] = typer.Option(
|