Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions agentkit/toolkit/cli/cli_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@ def _is_harness_runtime(runtime: rt.AgentKitRuntimesForListRuntimes) -> bool:
return False


# Fixed ADK app name the harness loader serves its agent under (the deployed
# HARNESS_NAME is irrelevant to the ADK app path).
_HARNESS_APP = "harness"


def _user_id_from_token(token: str) -> Optional[str]:
"""Return the OIDC ``sub`` claim from a JWT bearer token, else ``None``.

A custom_jwt harness is reached with an OIDC id_token whose ``sub`` is the
authenticated user's stable id. A key_auth token is an opaque api key (not a
JWT, no ``sub``), so this returns ``None`` and the caller must be given an
explicit ``--user-id``.

NOTE: this mirrors ``cli_invoke._user_id_from_token``; consolidate the two
into one shared helper once the ``invoke harness`` run_sse work lands.
"""
import base64
import binascii
import json

parts = token.split(".")
if len(parts) != 3:
return None # not a JWT (e.g. a key_auth api key)
payload = parts[1]
payload += "=" * (-len(payload) % 4) # restore base64 padding
try:
claims = json.loads(base64.urlsafe_b64decode(payload))
except (ValueError, binascii.Error):
return None # malformed JWT payload
sub = claims.get("sub")
return sub if isinstance(sub, str) and sub else None


@list_app.command("harness")
def list_harness_command(
region: Optional[str] = typer.Option(
Expand Down Expand Up @@ -129,6 +162,173 @@ def build_request(next_token_val):
local_console.print(table)


def _resolve_harness_endpoint(harness: str, directory: str, apikey: Optional[str]):
"""Resolve a deployed harness's data-plane base URL and bearer token.

Mirrors ``invoke harness``'s auth-type-aware credential selection:
1. ``--apikey`` always wins (explicit override);
2. a custom_jwt harness (no stored ``key``) → the ``agentkit login`` OIDC
id_token (auto-refreshed), whose ``sub`` identifies the caller;
3. a key_auth harness → its static ``key`` (an opaque api key, not a JWT).

Returns ``(base_url, token)``. Fast-fails when the harness is not in the
registry, or when a needed login id_token cannot be refreshed.
"""
from agentkit.toolkit.harness import load_harness_registry

registry = load_harness_registry(directory)
entry = registry.get(harness)
if not isinstance(entry, dict) or not entry.get("url"):
from pathlib import Path

registry_path = Path(directory).resolve() / "harness.json"
console.print(
f"[red]Error: harness '{harness}' not found in registry {registry_path}. "
f"Deploy it first with `agentkit deploy --harness {harness}`.[/red]"
)
raise typer.Exit(1)

base_url = entry["url"].rstrip("/")

from agentkit.auth.errors import AuthError
from agentkit.auth.sso import load_session

is_jwt_harness = entry.get("auth_type") == "custom_jwt" or not entry.get("key")
token = apikey or ""
if not token and is_jwt_harness:
try:
auth_session = load_session()
except Exception:
auth_session = None
if auth_session is not None:
try:
token = auth_session.valid_id_token()
except AuthError as e:
console.print(f"[red]❌ {e}[/red]")
if getattr(e, "hint", None):
console.print(f"[yellow]{e.hint}[/yellow]")
raise typer.Exit(1)
if not token:
token = entry.get("key") or ""

return base_url, token


@list_app.command("sessions")
def list_sessions_command(
harness: str = typer.Option(
..., "--harness", help="Harness name (resolved via the harness.json registry)."
),
user_id: Optional[str] = typer.Option(
None,
"--user-id",
help=(
"user_id whose sessions to list. If omitted, it is taken from the "
"JWT `sub` of the harness credential; a key_auth harness has no JWT, "
"so --user-id is required there."
),
),
apikey: Optional[str] = typer.Option(
None,
"--apikey",
"-ak",
help="Bearer token (e.g. a custom_jwt harness's OIDC JWT) overriding the registry credential.",
),
directory: str = typer.Option(
".", "--directory", help="Directory containing harness.json."
),
output: str = typer.Option(
"table", "--output", help="Output format: table|json|yaml"
),
quiet: bool = typer.Option(
False, "--quiet", "-q", help="Print only session ids"
),
no_color: bool = typer.Option(
False, "--no-color", "-nc", help="Disable colored output for tables/panels"
),
):
"""List a user's conversation sessions on a deployed harness runtime.

Calls the harness runtime's ADK endpoint
``GET /apps/harness/users/{user_id}/sessions``. ADK requires a user_id (there
is no cross-user listing), so it must be given explicitly via --user-id or be
derivable from the JWT `sub` of the harness credential.
"""
import json
from datetime import datetime

import requests
from rich.table import Table

local_console = console if not no_color else Console(no_color=True)

base_url, token = _resolve_harness_endpoint(harness, directory, apikey)

resolved_user_id = user_id or _user_id_from_token(token)
if not resolved_user_id:
local_console.print(
"[red]Error: cannot determine user_id — no --user-id given and the "
"harness credential is not a JWT with a `sub` claim (e.g. a key_auth "
"harness). Pass --user-id explicitly.[/red]"
)
raise typer.Exit(1)

url = f"{base_url}/apps/{_HARNESS_APP}/users/{resolved_user_id}/sessions"
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"

with local_console.status("[cyan]Fetching sessions...[/cyan]", spinner="dots"):
try:
resp = requests.get(url, headers=headers, timeout=60)
except requests.RequestException as e:
local_console.print(f"[red]❌ List sessions request failed: {e}[/red]")
raise typer.Exit(1)

if resp.status_code != 200:
local_console.print(
f"[red]❌ List sessions HTTP {resp.status_code}: {resp.text[:300]}[/red]"
)
raise typer.Exit(1)

sessions = resp.json() or []

if quiet:
for s in sessions:
local_console.print(s.get("id", ""))
return

if output.lower() == "json":
local_console.print(json.dumps(sessions, indent=2, ensure_ascii=False))
return
if output.lower() == "yaml":
import yaml

local_console.print(yaml.safe_dump(sessions, sort_keys=False, allow_unicode=True))
return

table = Table(
title=f"Sessions for user '{resolved_user_id}' (Count: {len(sessions)})",
show_lines=False,
)
table.add_column("SessionId", style="cyan")
table.add_column("Events", style="green")
table.add_column("LastUpdate", style="magenta")
for s in sessions:
ts = s.get("lastUpdateTime")
last_update = (
datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
if isinstance(ts, (int, float)) and ts
else ""
)
table.add_row(
str(s.get("id", "")),
str(len(s.get("events", []))),
last_update,
)
local_console.print(table)


@list_app.command("credentials")
def list_credentials_command(
region: Optional[str] = typer.Option(
Expand Down
50 changes: 50 additions & 0 deletions docs/content/2.agentkit-cli/5.harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,53 @@ agentkit deploy --harness my-harness \
agentkit list harness
agentkit list harness --output json --region cn-beijing
```

## 8. list sessions —— 列出某 Harness 的会话

`agentkit list sessions --harness <name>` 列出某个已部署 harness 运行时上、**某个用户**的对话会话。它通过 `harness.json` 解析出运行时地址与凭证,直接调用运行时的 ADK 数据面端点:

```text
GET {harness_url}/apps/harness/users/{user_id}/sessions
```

> 与 `list harness`(控制面,AK/SK 调火山 `ListRuntimes`)不同,本命令是**数据面**调用,寻址方式与 `invoke harness` 一致(按名从 `harness.json` 解析、用 harness 凭证鉴权)。

### user_id 的确定方式

ADK 的会话列表接口把 `user_id` 作为**必填路径参数**,没有"列出所有用户会话"的能力。因此 user_id 必须能被确定,按以下优先级:

1. 显式 `--user-id <id>`;
2. 未指定时,从 harness 凭证的 JWT `sub` 解出——
- **custom_jwt(OAuth)harness**:默认使用 `agentkit login` 的 id_token(或 `-ak` 传入的 JWT),其 `sub` 即用户身份;
- **key_auth harness**:凭证是不透明 API Key、不是 JWT,**无法解出 user_id**,此时必须显式 `--user-id`,否则快速失败。

### 参数

| 参数 | 说明 | 默认值 |
| :--- | :--- | :--- |
| `--harness <name>` | **(必填)** harness 名称,从 `harness.json` 解析。 | — |
| `--user-id <id>` | 要查询的用户;缺省时尝试从凭证 JWT 的 `sub` 推导。 | 由 JWT 推导 |
| `--apikey`, `-ak <token>` | Bearer token(如 custom_jwt harness 的 OIDC JWT),覆盖 registry 中的凭证。 | 注册表凭证 |
| `--directory <dir>` | `harness.json` 所在目录。 | `.`(当前目录) |
| `--output` | 输出格式 `table` / `json` / `yaml`。 | `table` |
| `--quiet`, `-q` | 仅打印会话 id。 | `false` |
| `--no-color`, `-nc` | 关闭彩色输出。 | `false` |

表格输出包含 `SessionId`、`Events`(事件数)、`LastUpdate`(最近更新时间)等列。

### 例子

```bash
# custom_jwt(OAuth)harness:已 agentkit login,user_id 自动取自 JWT sub
agentkit list sessions --harness my-harness

# 显式指定用户(key_auth harness 必须这样)
agentkit list sessions --harness my-harness --user-id alice

# 传入 JWT 覆盖凭证(user_id 取自该 JWT 的 sub)
agentkit list sessions --harness my-harness -ak "<oidc-jwt>"

# 仅打印会话 id / JSON 输出
agentkit list sessions --harness my-harness --user-id alice --quiet
agentkit list sessions --harness my-harness --user-id alice --output json
```
Loading