kaos-cli 0.6.0__tar.gz → 0.7.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.
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/PKG-INFO +1 -1
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/__init__.py +1 -1
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/__init__.py +79 -10
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/deploy.py +24 -2
- kaos_cli-0.7.0/kaos_cli/agent/invoke.py +300 -0
- kaos_cli-0.7.0/kaos_cli/agent/memory.py +91 -0
- kaos_cli-0.7.0/kaos_cli/agent/tools.py +55 -0
- kaos_cli-0.7.0/kaos_cli/auth/__init__.py +79 -0
- kaos_cli-0.7.0/kaos_cli/auth/consent.py +193 -0
- kaos_cli-0.7.0/kaos_cli/auth/grant.py +155 -0
- kaos_cli-0.7.0/kaos_cli/auth/login.py +61 -0
- kaos_cli-0.7.0/kaos_cli/cluster_http.py +83 -0
- kaos_cli-0.7.0/kaos_cli/config.py +141 -0
- kaos_cli-0.7.0/kaos_cli/config_cli.py +56 -0
- kaos_cli-0.7.0/kaos_cli/install/__init__.py +540 -0
- kaos_cli-0.7.0/kaos_cli/install/auth.py +573 -0
- kaos_cli-0.7.0/kaos_cli/install/infra.py +268 -0
- kaos_cli-0.7.0/kaos_cli/install/observability.py +292 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/main.py +8 -0
- kaos_cli-0.7.0/kaos_cli/memory/__init__.py +399 -0
- kaos_cli-0.7.0/kaos_cli/memorystore/__init__.py +62 -0
- kaos_cli-0.7.0/kaos_cli/memorystore/create.py +72 -0
- kaos_cli-0.7.0/kaos_cli/samples/data/7-memory-agent.yaml +133 -0
- kaos_cli-0.7.0/kaos_cli/samples/data/8-authorization-walkthrough.yaml +78 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/__init__.py +253 -144
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/create_rbac.py +3 -16
- kaos_cli-0.7.0/kaos_cli/system/status.py +55 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/utils/__init__.py +27 -1
- kaos_cli-0.7.0/kaos_cli/utils/port_forward.py +66 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/PKG-INFO +1 -1
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/SOURCES.txt +24 -2
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/pyproject.toml +1 -1
- kaos_cli-0.7.0/tests/test_agent_invoke.py +128 -0
- kaos_cli-0.7.0/tests/test_agent_tools.py +72 -0
- kaos_cli-0.7.0/tests/test_auth_cli.py +273 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/tests/test_cli_integration.py +954 -47
- kaos_cli-0.7.0/tests/test_config_cli.py +39 -0
- kaos_cli-0.7.0/tests/test_install_surface.py +97 -0
- kaos_cli-0.7.0/tests/test_memory_cli.py +345 -0
- kaos_cli-0.6.0/kaos_cli/agent/invoke.py +0 -128
- kaos_cli-0.6.0/kaos_cli/agent/memory.py +0 -119
- kaos_cli-0.6.0/kaos_cli/install.py +0 -1512
- kaos_cli-0.6.0/kaos_cli/samples/data/7-memory-agent.yaml +0 -97
- kaos_cli-0.6.0/kaos_cli/system/status.py +0 -82
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/README.md +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/a2a.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/build.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/init.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/run.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/status.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/__init__.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/build.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/crud.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/deploy.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/init.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/invoke.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/run.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/__init__.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/deploy.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/invoke.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/proxy.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/__init__.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/install.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/runtimes.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/ui.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/utils/crud.py +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/dependency_links.txt +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/entry_points.txt +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/requires.txt +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/top_level.txt +0 -0
- {kaos_cli-0.6.0 → kaos_cli-0.7.0}/setup.cfg +0 -0
|
@@ -12,6 +12,7 @@ from kaos_cli.agent.deploy import deploy_agent
|
|
|
12
12
|
from kaos_cli.agent.invoke import invoke_command
|
|
13
13
|
from kaos_cli.agent.status import status_command
|
|
14
14
|
from kaos_cli.agent.memory import memory_command
|
|
15
|
+
from kaos_cli.agent.tools import tools_command
|
|
15
16
|
from kaos_cli.agent.init import init_command
|
|
16
17
|
from kaos_cli.agent.build import build_command
|
|
17
18
|
from kaos_cli.agent.run import run_command
|
|
@@ -46,7 +47,9 @@ def build_agent(
|
|
|
46
47
|
"agent:agent",
|
|
47
48
|
help="Module:object target (default: agent:agent). No .py extension.",
|
|
48
49
|
),
|
|
49
|
-
image: str = typer.Option(
|
|
50
|
+
image: str = typer.Option(
|
|
51
|
+
..., "--image", "-i", help="Image name with tag (e.g. my-agent:latest)."
|
|
52
|
+
),
|
|
50
53
|
directory: str = typer.Option(".", "--dir", "-d", help="Source directory."),
|
|
51
54
|
kind_load: bool = typer.Option(
|
|
52
55
|
False, "--kind-load", help="Load image to KIND cluster."
|
|
@@ -93,7 +96,9 @@ def run_agent(
|
|
|
93
96
|
),
|
|
94
97
|
host: str = typer.Option("0.0.0.0", "--host", help="Bind host."),
|
|
95
98
|
port: int = typer.Option(8000, "--port", "-p", help="Bind port."),
|
|
96
|
-
reload: bool = typer.Option(
|
|
99
|
+
reload: bool = typer.Option(
|
|
100
|
+
False, "--reload", "-r", help="Auto-reload on changes."
|
|
101
|
+
),
|
|
97
102
|
) -> None:
|
|
98
103
|
"""Run a Pydantic AI agent server locally."""
|
|
99
104
|
run_command(target=target, host=host, port=port, reload=reload)
|
|
@@ -244,13 +249,28 @@ def deploy_agent_cmd(
|
|
|
244
249
|
None, "--auto-interval", help="Seconds between autonomous iterations."
|
|
245
250
|
),
|
|
246
251
|
task_max_iterations: int = typer.Option(
|
|
247
|
-
None,
|
|
252
|
+
None,
|
|
253
|
+
"--task-max-iterations",
|
|
254
|
+
help="Max iterations for A2A async tasks (0=unlimited).",
|
|
248
255
|
),
|
|
249
256
|
task_max_runtime: int = typer.Option(
|
|
250
|
-
None,
|
|
257
|
+
None,
|
|
258
|
+
"--task-max-runtime",
|
|
259
|
+
help="Max runtime in seconds for A2A async tasks (0=unlimited).",
|
|
251
260
|
),
|
|
252
261
|
task_max_tool_calls: int = typer.Option(
|
|
253
|
-
None,
|
|
262
|
+
None,
|
|
263
|
+
"--task-max-tool-calls",
|
|
264
|
+
help="Max cumulative tool calls for A2A async tasks (0=unlimited).",
|
|
265
|
+
),
|
|
266
|
+
memory_store: str = typer.Option(
|
|
267
|
+
None, "--memory-store", help="MemoryStore reference."
|
|
268
|
+
),
|
|
269
|
+
memory_max_read_scope: str = typer.Option(
|
|
270
|
+
None, "--memory-max-read-scope", help="Maximum memory read scope."
|
|
271
|
+
),
|
|
272
|
+
memory_tools: str = typer.Option(
|
|
273
|
+
None, "--memory-tools", help="Memory tools to expose: read, write, or all."
|
|
254
274
|
),
|
|
255
275
|
dry_run: bool = typer.Option(
|
|
256
276
|
False, "--dry-run", help="Print YAML instead of deploying."
|
|
@@ -273,6 +293,17 @@ def deploy_agent_cmd(
|
|
|
273
293
|
typer.echo("Error: --build requires --image to be set", err=True)
|
|
274
294
|
sys.exit(1)
|
|
275
295
|
|
|
296
|
+
if not memory_store and any((memory_max_read_scope, memory_tools)):
|
|
297
|
+
typer.echo(
|
|
298
|
+
"Error: memory read scope and tools flags require --memory-store",
|
|
299
|
+
err=True,
|
|
300
|
+
)
|
|
301
|
+
raise typer.Exit(1)
|
|
302
|
+
if memory_tools not in (None, "read", "write", "all"):
|
|
303
|
+
raise typer.BadParameter(
|
|
304
|
+
"must be 'read', 'write', or 'all'", param_hint="--memory-tools"
|
|
305
|
+
)
|
|
306
|
+
|
|
276
307
|
# Run build if requested
|
|
277
308
|
if build is not None:
|
|
278
309
|
build_target = build if build else "agent:agent"
|
|
@@ -308,6 +339,9 @@ def deploy_agent_cmd(
|
|
|
308
339
|
task_max_iterations=task_max_iterations,
|
|
309
340
|
task_max_runtime=task_max_runtime,
|
|
310
341
|
task_max_tool_calls=task_max_tool_calls,
|
|
342
|
+
memory_store=memory_store,
|
|
343
|
+
memory_max_read_scope=memory_max_read_scope,
|
|
344
|
+
memory_tools=memory_tools,
|
|
311
345
|
wait=wait,
|
|
312
346
|
wait_timeout=wait_timeout,
|
|
313
347
|
dry_run=dry_run,
|
|
@@ -318,7 +352,7 @@ def deploy_agent_cmd(
|
|
|
318
352
|
def invoke_agent(
|
|
319
353
|
name: str = typer.Argument(..., help="Name of the Agent."),
|
|
320
354
|
message: str = typer.Option(
|
|
321
|
-
|
|
355
|
+
"Hello", "--message", "-m", help="Message to send to the agent."
|
|
322
356
|
),
|
|
323
357
|
namespace: str = typer.Option(
|
|
324
358
|
None,
|
|
@@ -338,10 +372,26 @@ def invoke_agent(
|
|
|
338
372
|
"-s",
|
|
339
373
|
help="Stream the response.",
|
|
340
374
|
),
|
|
375
|
+
user: str | None = typer.Option(
|
|
376
|
+
None,
|
|
377
|
+
"--user",
|
|
378
|
+
help="Invoke through the gateway with this user's cached token.",
|
|
379
|
+
),
|
|
380
|
+
session: str = typer.Option(
|
|
381
|
+
None,
|
|
382
|
+
"--session",
|
|
383
|
+
help="Conversation session ID.",
|
|
384
|
+
),
|
|
341
385
|
) -> None:
|
|
342
|
-
"""Send a message to an Agent
|
|
386
|
+
"""Send a message to an Agent directly or through the configured gateway."""
|
|
343
387
|
invoke_command(
|
|
344
|
-
name=name,
|
|
388
|
+
name=name,
|
|
389
|
+
namespace=namespace,
|
|
390
|
+
message=message,
|
|
391
|
+
port=port,
|
|
392
|
+
stream=stream,
|
|
393
|
+
user=user,
|
|
394
|
+
session=session,
|
|
345
395
|
)
|
|
346
396
|
|
|
347
397
|
|
|
@@ -367,7 +417,7 @@ def status_agent(
|
|
|
367
417
|
),
|
|
368
418
|
) -> None:
|
|
369
419
|
"""Get agent status and capabilities via agent card.
|
|
370
|
-
|
|
420
|
+
|
|
371
421
|
Examples:
|
|
372
422
|
kaos agent status my-agent
|
|
373
423
|
kaos agent status my-agent --json
|
|
@@ -404,7 +454,7 @@ def memory_agent(
|
|
|
404
454
|
),
|
|
405
455
|
) -> None:
|
|
406
456
|
"""Get agent memory events.
|
|
407
|
-
|
|
457
|
+
|
|
408
458
|
Examples:
|
|
409
459
|
kaos agent memory my-agent
|
|
410
460
|
kaos agent memory my-agent --json
|
|
@@ -417,3 +467,22 @@ def memory_agent(
|
|
|
417
467
|
port=port,
|
|
418
468
|
output_json=output_json,
|
|
419
469
|
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@app.command(name="tools")
|
|
473
|
+
def tools_agent(
|
|
474
|
+
name: str = typer.Argument(..., help="Name of the Agent."),
|
|
475
|
+
namespace: str = typer.Option(
|
|
476
|
+
None,
|
|
477
|
+
"--namespace",
|
|
478
|
+
"-n",
|
|
479
|
+
help="Namespace of the Agent.",
|
|
480
|
+
),
|
|
481
|
+
output_json: bool = typer.Option(
|
|
482
|
+
False,
|
|
483
|
+
"--json",
|
|
484
|
+
help="Output in JSON format.",
|
|
485
|
+
),
|
|
486
|
+
) -> None:
|
|
487
|
+
"""Show the tool definitions presented to the model."""
|
|
488
|
+
tools_command(name=name, namespace=namespace, output_json=output_json)
|
|
@@ -56,6 +56,9 @@ def deploy_agent(
|
|
|
56
56
|
task_max_iterations: int | None = None,
|
|
57
57
|
task_max_runtime: int | None = None,
|
|
58
58
|
task_max_tool_calls: int | None = None,
|
|
59
|
+
memory_store: str | None = None,
|
|
60
|
+
memory_max_read_scope: str | None = None,
|
|
61
|
+
memory_tools: str | None = None,
|
|
59
62
|
wait: bool = False,
|
|
60
63
|
wait_timeout: int = DEFAULT_WAIT_TIMEOUT,
|
|
61
64
|
dry_run: bool = False,
|
|
@@ -68,8 +71,19 @@ def deploy_agent(
|
|
|
68
71
|
)
|
|
69
72
|
|
|
70
73
|
# Add config section if description, instructions, telemetry, autonomous, or task budgets provided
|
|
71
|
-
has_task_budgets =
|
|
72
|
-
|
|
74
|
+
has_task_budgets = (
|
|
75
|
+
task_max_iterations is not None
|
|
76
|
+
or task_max_runtime is not None
|
|
77
|
+
or task_max_tool_calls is not None
|
|
78
|
+
)
|
|
79
|
+
has_config = (
|
|
80
|
+
description
|
|
81
|
+
or instructions
|
|
82
|
+
or otel_endpoint
|
|
83
|
+
or autonomous
|
|
84
|
+
or has_task_budgets
|
|
85
|
+
or memory_store
|
|
86
|
+
)
|
|
73
87
|
if has_config:
|
|
74
88
|
yaml_content += " config:\n"
|
|
75
89
|
if description:
|
|
@@ -98,6 +112,14 @@ def deploy_agent(
|
|
|
98
112
|
yaml_content += f" maxRuntimeSeconds: {task_max_runtime}\n"
|
|
99
113
|
if task_max_tool_calls is not None:
|
|
100
114
|
yaml_content += f" maxToolCalls: {task_max_tool_calls}\n"
|
|
115
|
+
if memory_store:
|
|
116
|
+
yaml_content += " memory:\n"
|
|
117
|
+
yaml_content += " type: remote\n"
|
|
118
|
+
yaml_content += f" memoryStore: {memory_store}\n"
|
|
119
|
+
if memory_max_read_scope:
|
|
120
|
+
yaml_content += f" maxReadScope: {memory_max_read_scope}\n"
|
|
121
|
+
if memory_tools:
|
|
122
|
+
yaml_content += f" tools: {memory_tools}\n"
|
|
101
123
|
|
|
102
124
|
# Add MCP servers if provided
|
|
103
125
|
if mcp_servers:
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""KAOS Agent invoke command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import signal
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kaos_cli.cluster_http import local_service_url
|
|
11
|
+
from kaos_cli.config import load_config, session_token
|
|
12
|
+
from kaos_cli.utils import current_context_namespace
|
|
13
|
+
from kaos_cli.auth.consent import (
|
|
14
|
+
active_service_alias,
|
|
15
|
+
reauth_url,
|
|
16
|
+
service_alias,
|
|
17
|
+
service_id_from_reauth_url,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
REASON_TEXT = {
|
|
22
|
+
"platform_grant_missing": "not granted",
|
|
23
|
+
"user_grant_required": "user not in a granted group",
|
|
24
|
+
"access-control unreachable": "access-control unavailable (failing closed)",
|
|
25
|
+
"access_control_unreachable": "access-control unavailable (failing closed)",
|
|
26
|
+
"missing token": "no valid identity",
|
|
27
|
+
"missing_token": "no valid identity",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def plain_access_reason(reason: str) -> str:
|
|
32
|
+
"""Translate gateway reason codes into concise user-facing text."""
|
|
33
|
+
normalized = reason.strip().lower()
|
|
34
|
+
if normalized in REASON_TEXT:
|
|
35
|
+
return REASON_TEXT[normalized]
|
|
36
|
+
if "access-control" in normalized and "unreachable" in normalized:
|
|
37
|
+
return REASON_TEXT["access-control unreachable"]
|
|
38
|
+
if "missing" in normalized and "token" in normalized:
|
|
39
|
+
return REASON_TEXT["missing token"]
|
|
40
|
+
return reason.replace("_", " ") or "request permitted"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _response_content(response) -> str:
|
|
44
|
+
try:
|
|
45
|
+
data = response.json()
|
|
46
|
+
choices = data.get("choices", [])
|
|
47
|
+
if choices:
|
|
48
|
+
return choices[0].get("message", {}).get("content", "")
|
|
49
|
+
return json.dumps(data, indent=2)
|
|
50
|
+
except (ValueError, AttributeError):
|
|
51
|
+
return response.text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _access_control_ready(namespace: str) -> bool:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[
|
|
57
|
+
"kubectl",
|
|
58
|
+
"get",
|
|
59
|
+
"deployment/kaos-pdp",
|
|
60
|
+
"-n",
|
|
61
|
+
namespace,
|
|
62
|
+
"-o",
|
|
63
|
+
"jsonpath={.status.readyReplicas}/{.spec.replicas}",
|
|
64
|
+
],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
)
|
|
68
|
+
if result.returncode != 0:
|
|
69
|
+
return False
|
|
70
|
+
ready, _, desired = result.stdout.partition("/")
|
|
71
|
+
return bool(ready) and ready == desired and desired != "0"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _invoke_gateway(
|
|
75
|
+
name: str, namespace: str | None, message: str, user: str | None, session: str | None = None
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Invoke an agent through the configured gateway and print its verdict."""
|
|
78
|
+
import httpx
|
|
79
|
+
|
|
80
|
+
config = load_config()
|
|
81
|
+
address = config["gateway"].get("address", "").rstrip("/")
|
|
82
|
+
namespace = namespace or config.get("namespace") or "default"
|
|
83
|
+
if not address:
|
|
84
|
+
typer.echo("✗ denied — no gateway address configured")
|
|
85
|
+
return
|
|
86
|
+
token = session_token(config, user) if user else None
|
|
87
|
+
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
88
|
+
if session:
|
|
89
|
+
headers["X-Session-ID"] = session
|
|
90
|
+
try:
|
|
91
|
+
with local_service_url(address) as local_address:
|
|
92
|
+
response = httpx.post(
|
|
93
|
+
f"{local_address}/{namespace}/agent/{name}/v1/chat/completions",
|
|
94
|
+
headers=headers,
|
|
95
|
+
json={"messages": [{"role": "user", "content": message}], "stream": False},
|
|
96
|
+
timeout=120.0,
|
|
97
|
+
)
|
|
98
|
+
except (httpx.HTTPError, RuntimeError) as exc:
|
|
99
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
100
|
+
return
|
|
101
|
+
content = _response_content(response)
|
|
102
|
+
approval_url = reauth_url(response)
|
|
103
|
+
if approval_url:
|
|
104
|
+
service_id = service_id_from_reauth_url(approval_url)
|
|
105
|
+
try:
|
|
106
|
+
service = service_alias(config, service_id or "")
|
|
107
|
+
except (httpx.HTTPError, RuntimeError, ValueError):
|
|
108
|
+
service = "service"
|
|
109
|
+
typer.echo(
|
|
110
|
+
f"✗ needs approval — run: kaos auth connect {service} --user {user}"
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
if content:
|
|
114
|
+
typer.echo(content)
|
|
115
|
+
reason = response.headers.get("x-kaos-access-reason", "")
|
|
116
|
+
allowed = response.status_code < 400
|
|
117
|
+
if not reason:
|
|
118
|
+
if response.status_code in (401, 403) and not token:
|
|
119
|
+
reason = "missing token"
|
|
120
|
+
elif response.status_code in (401, 403) and not _access_control_ready(namespace):
|
|
121
|
+
reason = "access-control unreachable"
|
|
122
|
+
elif response.status_code in (401, 403) and token:
|
|
123
|
+
reason = "user_grant_required"
|
|
124
|
+
elif response.status_code >= 500:
|
|
125
|
+
reason = "access-control unreachable"
|
|
126
|
+
else:
|
|
127
|
+
reason = "request permitted" if allowed else f"HTTP {response.status_code}"
|
|
128
|
+
if allowed and user:
|
|
129
|
+
try:
|
|
130
|
+
service = active_service_alias(config, user, message)
|
|
131
|
+
except (httpx.HTTPError, RuntimeError, ValueError):
|
|
132
|
+
service = None
|
|
133
|
+
if service:
|
|
134
|
+
typer.echo(f"✓ allowed — acting as {user} on {service}")
|
|
135
|
+
return
|
|
136
|
+
mark = "✓ allowed" if allowed else "✗ denied"
|
|
137
|
+
typer.echo(f"{mark} — {plain_access_reason(reason)}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _is_autonomous(name: str, namespace: str | None) -> bool:
|
|
141
|
+
cmd = [
|
|
142
|
+
"kubectl",
|
|
143
|
+
"get",
|
|
144
|
+
"agent",
|
|
145
|
+
name,
|
|
146
|
+
"-o",
|
|
147
|
+
"jsonpath={.spec.config.autonomous.goal}",
|
|
148
|
+
]
|
|
149
|
+
if namespace:
|
|
150
|
+
cmd.extend(["-n", namespace])
|
|
151
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
152
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def invoke_command(
|
|
156
|
+
name: str,
|
|
157
|
+
namespace: str | None,
|
|
158
|
+
message: str,
|
|
159
|
+
port: int,
|
|
160
|
+
stream: bool,
|
|
161
|
+
user: str | None = None,
|
|
162
|
+
session: str | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Send a message to an Agent via port-forward."""
|
|
165
|
+
import httpx
|
|
166
|
+
|
|
167
|
+
config = load_config()
|
|
168
|
+
direct_namespace = namespace or current_context_namespace()
|
|
169
|
+
if user or (
|
|
170
|
+
config["gateway"].get("through_gateway")
|
|
171
|
+
and not _is_autonomous(name, direct_namespace)
|
|
172
|
+
):
|
|
173
|
+
_invoke_gateway(name, namespace, message, user, session)
|
|
174
|
+
return
|
|
175
|
+
namespace = direct_namespace
|
|
176
|
+
|
|
177
|
+
# Find the service for this Agent
|
|
178
|
+
cmd = [
|
|
179
|
+
"kubectl",
|
|
180
|
+
"get",
|
|
181
|
+
"svc",
|
|
182
|
+
f"agent-{name}",
|
|
183
|
+
"-o",
|
|
184
|
+
"jsonpath={.spec.ports[0].port}",
|
|
185
|
+
]
|
|
186
|
+
if namespace:
|
|
187
|
+
cmd.extend(["-n", namespace])
|
|
188
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
189
|
+
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
typer.echo(f"Error: Agent '{name}' not found", err=True)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
svc_port = result.stdout.strip() or "8000"
|
|
195
|
+
|
|
196
|
+
typer.echo(f"Port-forwarding to agent-{name}:{svc_port}...")
|
|
197
|
+
|
|
198
|
+
# Start port-forward in background
|
|
199
|
+
pf_cmd = ["kubectl", "port-forward", f"svc/agent-{name}", f"{port}:{svc_port}"]
|
|
200
|
+
if namespace:
|
|
201
|
+
pf_cmd.extend(["-n", namespace])
|
|
202
|
+
pf_process = subprocess.Popen(
|
|
203
|
+
pf_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def cleanup():
|
|
207
|
+
pf_process.terminate()
|
|
208
|
+
pf_process.wait()
|
|
209
|
+
|
|
210
|
+
signal.signal(signal.SIGINT, lambda s, f: (cleanup(), sys.exit(0)))
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
last_error = "port-forward did not become ready"
|
|
214
|
+
for _ in range(90):
|
|
215
|
+
if pf_process.poll() is not None:
|
|
216
|
+
stderr = pf_process.stderr.read().decode() if pf_process.stderr else ""
|
|
217
|
+
typer.echo(f"Error: Port-forward failed: {stderr}", err=True)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
try:
|
|
220
|
+
health = httpx.get(f"http://localhost:{port}/health", timeout=2.0)
|
|
221
|
+
if health.status_code == 200:
|
|
222
|
+
break
|
|
223
|
+
last_error = f"HTTP {health.status_code}"
|
|
224
|
+
except httpx.HTTPError as exc:
|
|
225
|
+
last_error = str(exc)
|
|
226
|
+
time.sleep(1)
|
|
227
|
+
else:
|
|
228
|
+
typer.echo(f"Error: Could not connect to Agent: {last_error}", err=True)
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
typer.echo(f"Sending message: {message}")
|
|
232
|
+
headers = {"X-Session-ID": session} if session else None
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if stream:
|
|
236
|
+
# Streaming response
|
|
237
|
+
with httpx.stream(
|
|
238
|
+
"POST",
|
|
239
|
+
f"http://localhost:{port}/v1/chat/completions",
|
|
240
|
+
json={
|
|
241
|
+
"messages": [{"role": "user", "content": message}],
|
|
242
|
+
"stream": True,
|
|
243
|
+
},
|
|
244
|
+
headers=headers,
|
|
245
|
+
timeout=120.0,
|
|
246
|
+
) as response:
|
|
247
|
+
typer.echo("\n📤 Response:")
|
|
248
|
+
for line in response.iter_lines():
|
|
249
|
+
if line.startswith("data: "):
|
|
250
|
+
data = line[6:]
|
|
251
|
+
if data != "[DONE]":
|
|
252
|
+
try:
|
|
253
|
+
chunk = json.loads(data)
|
|
254
|
+
if "choices" in chunk and chunk["choices"]:
|
|
255
|
+
delta = chunk["choices"][0].get("delta", {})
|
|
256
|
+
# Handle reasoning content (displayed before regular content)
|
|
257
|
+
if "content" in delta:
|
|
258
|
+
content = delta["content"]
|
|
259
|
+
if content.startswith("{"):
|
|
260
|
+
content = json.loads(content)
|
|
261
|
+
typer.echo(f'Reasoning step {content["step"]}/{content["max_steps"]} | Action: {content["action"]} | Target: {content["target"]}')
|
|
262
|
+
else:
|
|
263
|
+
typer.echo(content, nl=False)
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
pass
|
|
266
|
+
typer.echo("") # Final newline
|
|
267
|
+
else:
|
|
268
|
+
# Non-streaming
|
|
269
|
+
response = httpx.post(
|
|
270
|
+
f"http://localhost:{port}/v1/chat/completions",
|
|
271
|
+
json={
|
|
272
|
+
"messages": [{"role": "user", "content": message}],
|
|
273
|
+
"stream": False,
|
|
274
|
+
},
|
|
275
|
+
headers=headers,
|
|
276
|
+
timeout=120.0,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if response.status_code == 200:
|
|
280
|
+
result = response.json()
|
|
281
|
+
if "choices" in result and result["choices"]:
|
|
282
|
+
content = (
|
|
283
|
+
result["choices"][0].get("message", {}).get("content", "")
|
|
284
|
+
)
|
|
285
|
+
typer.echo("\n📤 Response:")
|
|
286
|
+
typer.echo(content)
|
|
287
|
+
else:
|
|
288
|
+
typer.echo(json.dumps(result, indent=2))
|
|
289
|
+
typer.echo("✓ allowed — request permitted")
|
|
290
|
+
else:
|
|
291
|
+
typer.echo(
|
|
292
|
+
f"Error: HTTP {response.status_code}: {response.text}", err=True
|
|
293
|
+
)
|
|
294
|
+
except httpx.ConnectError:
|
|
295
|
+
typer.echo("Error: Could not connect to Agent", err=True)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
typer.echo(f"Error: {e}", err=True)
|
|
299
|
+
finally:
|
|
300
|
+
cleanup()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""KAOS Agent memory command - get agent memory events."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from kaos_cli.utils.port_forward import PortForwardError, port_forward
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def memory_command(
|
|
12
|
+
name: str,
|
|
13
|
+
namespace: str | None,
|
|
14
|
+
session_id: str | None,
|
|
15
|
+
port: int,
|
|
16
|
+
output_json: bool,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Get agent memory events via port-forward."""
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
# Find the service for this Agent
|
|
22
|
+
cmd = [
|
|
23
|
+
"kubectl",
|
|
24
|
+
"get",
|
|
25
|
+
"svc",
|
|
26
|
+
f"agent-{name}",
|
|
27
|
+
"-o",
|
|
28
|
+
"jsonpath={.spec.ports[0].port}",
|
|
29
|
+
]
|
|
30
|
+
if namespace:
|
|
31
|
+
cmd.extend(["-n", namespace])
|
|
32
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
33
|
+
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
typer.echo(f"Error: Agent '{name}' not found", err=True)
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
svc_port = result.stdout.strip() or "8000"
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with port_forward(
|
|
42
|
+
f"svc/agent-{name}",
|
|
43
|
+
int(svc_port),
|
|
44
|
+
namespace,
|
|
45
|
+
"/health",
|
|
46
|
+
local_port=port,
|
|
47
|
+
) as base_url:
|
|
48
|
+
url = f"{base_url}/memory/events"
|
|
49
|
+
if session_id:
|
|
50
|
+
url += f"?session_id={session_id}"
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
response = httpx.get(url, timeout=10.0)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
memory = response.json()
|
|
56
|
+
except httpx.HTTPError as exc:
|
|
57
|
+
typer.echo(f"Error: Failed to get memory events: {exc}", err=True)
|
|
58
|
+
raise typer.Exit(1)
|
|
59
|
+
|
|
60
|
+
if output_json:
|
|
61
|
+
typer.echo(json.dumps(memory, indent=2))
|
|
62
|
+
else:
|
|
63
|
+
events = memory.get("events", [])
|
|
64
|
+
typer.echo(f"Memory Events: {len(events)} total")
|
|
65
|
+
|
|
66
|
+
event_types: dict[str, int] = {}
|
|
67
|
+
for event in events:
|
|
68
|
+
etype = event.get("event_type", "unknown")
|
|
69
|
+
event_types[etype] = event_types.get(etype, 0) + 1
|
|
70
|
+
|
|
71
|
+
if event_types:
|
|
72
|
+
typer.echo("Event Types:")
|
|
73
|
+
for etype, count in sorted(event_types.items()):
|
|
74
|
+
typer.echo(f" - {etype}: {count}")
|
|
75
|
+
|
|
76
|
+
if events:
|
|
77
|
+
typer.echo("\nAll Events:")
|
|
78
|
+
for event in events:
|
|
79
|
+
etype = event.get("event_type", "unknown")
|
|
80
|
+
content = event.get("content", "")
|
|
81
|
+
if isinstance(content, dict):
|
|
82
|
+
content = json.dumps(content)
|
|
83
|
+
typer.echo(f" [{etype}] {content}")
|
|
84
|
+
except PortForwardError as exc:
|
|
85
|
+
typer.echo(f"Error: Port-forward failed: {exc}", err=True)
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
except typer.Exit:
|
|
88
|
+
raise
|
|
89
|
+
except Exception as e:
|
|
90
|
+
typer.echo(f"Error: {e}", err=True)
|
|
91
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Inspect the tool definitions exposed by a deployed Agent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kaos_cli.utils.port_forward import PortForwardError, port_forward
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tools_command(name: str, namespace: str | None, output_json: bool) -> None:
|
|
13
|
+
"""Fetch and print an Agent's model-facing tool definitions."""
|
|
14
|
+
cmd = [
|
|
15
|
+
"kubectl",
|
|
16
|
+
"get",
|
|
17
|
+
"svc",
|
|
18
|
+
f"agent-{name}",
|
|
19
|
+
"-o",
|
|
20
|
+
"jsonpath={.spec.ports[0].port}",
|
|
21
|
+
]
|
|
22
|
+
if namespace:
|
|
23
|
+
cmd.extend(["-n", namespace])
|
|
24
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
25
|
+
if result.returncode != 0:
|
|
26
|
+
typer.echo(f"Error: Agent '{name}' not found", err=True)
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with port_forward(
|
|
31
|
+
f"svc/agent-{name}",
|
|
32
|
+
int(result.stdout.strip() or "8000"),
|
|
33
|
+
namespace,
|
|
34
|
+
"/health",
|
|
35
|
+
) as base_url:
|
|
36
|
+
response = httpx.get(f"{base_url}/tools", timeout=30.0)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
data = response.json()
|
|
39
|
+
except (PortForwardError, httpx.HTTPError, json.JSONDecodeError) as exc:
|
|
40
|
+
typer.echo(f"Error: Could not list Agent tools: {exc}", err=True)
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
if output_json:
|
|
44
|
+
typer.echo(json.dumps(data, indent=2))
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
tools = data.get("tools", [])
|
|
48
|
+
typer.echo(f"Tools for {data.get('agent', name)}: {len(tools)}")
|
|
49
|
+
for tool in tools:
|
|
50
|
+
typer.echo(f"\n{tool.get('name', '')}")
|
|
51
|
+
if tool.get("description"):
|
|
52
|
+
typer.echo(f" {tool['description']}")
|
|
53
|
+
schema = json.dumps(tool.get("parameters_json_schema", {}), indent=2)
|
|
54
|
+
typer.echo(" Schema:")
|
|
55
|
+
typer.echo("\n".join(f" {line}" for line in schema.splitlines()))
|