agentstack-cli 0.4.0__py3-none-macosx_12_0_arm64.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,181 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import datetime
5
+ import functools
6
+ import importlib.resources
7
+ import os
8
+ import pathlib
9
+ import platform
10
+ import shutil
11
+ import sys
12
+ import textwrap
13
+ import typing
14
+
15
+ import httpx
16
+ import typer
17
+ from agentstack_sdk.platform import Provider
18
+ from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, wait_fixed
19
+
20
+ from agentstack_cli.async_typer import AsyncTyper
21
+ from agentstack_cli.commands.platform.base_driver import BaseDriver
22
+ from agentstack_cli.commands.platform.lima_driver import LimaDriver
23
+ from agentstack_cli.commands.platform.wsl_driver import WSLDriver
24
+ from agentstack_cli.console import console
25
+ from agentstack_cli.utils import verbosity
26
+
27
+ app = AsyncTyper()
28
+
29
+
30
+ @functools.cache
31
+ def get_driver(vm_name: str = "agentstack") -> BaseDriver:
32
+ has_lima = (importlib.resources.files("agentstack_cli") / "data" / "limactl").is_file() or shutil.which("limactl")
33
+ has_vz = os.path.exists("/System/Library/Frameworks/Virtualization.framework")
34
+ arch = "aarch64" if platform.machine().lower() == "arm64" else platform.machine().lower()
35
+ has_qemu = bool(shutil.which(f"qemu-system-{arch}"))
36
+
37
+ if platform.system() == "Windows" or shutil.which("wsl.exe"):
38
+ return WSLDriver(vm_name=vm_name)
39
+ elif has_lima and (has_vz or has_qemu):
40
+ return LimaDriver(vm_name=vm_name)
41
+ else:
42
+ console.error("Could not find a compatible VM runtime.")
43
+ if platform.system() == "Darwin":
44
+ console.hint("This version of macOS is unsupported, please update the system.")
45
+ elif platform.system() == "Linux":
46
+ if not has_lima:
47
+ console.hint(
48
+ "This Linux distribution is not suppored by Lima VM binary releases (required: glibc>=2.34). Manually install Lima VM >=1.2.1 through either:\n"
49
+ + " - Your distribution's package manager, if available (https://repology.org/project/lima/versions)\n"
50
+ + " - Homebrew, which uses its own separate glibc on Linux (https://brew.sh)\n"
51
+ + " - Building it yourself, and ensuring that limactl is in PATH (https://lima-vm.io/docs/installation/source/)"
52
+ )
53
+ if not has_qemu:
54
+ console.hint(
55
+ f"QEMU is needed on Linux, please install it and ensure that qemu-system-{arch} is in PATH. Refer to https://www.qemu.org/download/ for instructions."
56
+ )
57
+ sys.exit(1)
58
+
59
+
60
+ @app.command("start")
61
+ async def start(
62
+ set_values_list: typing.Annotated[
63
+ list[str], typer.Option("--set", help="Set Helm chart values using <key>=<value> syntax", default_factory=list)
64
+ ],
65
+ import_images: typing.Annotated[
66
+ list[str],
67
+ typer.Option(
68
+ "--import", help="Import an image from a local Docker CLI into Agent Stack platform", default_factory=list
69
+ ),
70
+ ],
71
+ values_file: typing.Annotated[
72
+ pathlib.Path | None, typer.Option("-f", help="Set Helm chart values using yaml values file")
73
+ ] = None,
74
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
75
+ verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
76
+ ):
77
+ """Start Agent Stack platform."""
78
+ import agentstack_cli.commands.server
79
+
80
+ values_file_path = None
81
+ if values_file:
82
+ values_file_path = pathlib.Path(values_file)
83
+ if not values_file_path.is_file():
84
+ raise FileNotFoundError(f"Values file {values_file} not found.")
85
+
86
+ with verbosity(verbose):
87
+ driver = get_driver(vm_name=vm_name)
88
+ await driver.create_vm()
89
+ await driver.install_tools()
90
+ await driver.deploy(set_values_list=set_values_list, values_file=values_file_path, import_images=import_images)
91
+
92
+ with console.status("Waiting for Agent Stack platform to be ready...", spinner="dots"):
93
+ timeout = datetime.timedelta(minutes=20)
94
+ try:
95
+ async for attempt in AsyncRetrying(
96
+ stop=stop_after_delay(timeout),
97
+ wait=wait_fixed(datetime.timedelta(seconds=1)),
98
+ retry=retry_if_exception_type((httpx.HTTPError, ConnectionError)),
99
+ reraise=True,
100
+ ):
101
+ with attempt:
102
+ await Provider.list()
103
+ except Exception as ex:
104
+ raise ConnectionError(
105
+ f"Server did not start in {timeout}. Please check your internet connection."
106
+ ) from ex
107
+
108
+ console.success("Agent Stack platform started successfully!")
109
+
110
+ if any("phoenix.enabled=true" in value.lower() for value in set_values_list):
111
+ console.print(
112
+ textwrap.dedent("""\
113
+
114
+ License Notice:
115
+ When you enable Phoenix, be aware that Arize Phoenix is licensed under the Elastic License v2 (ELv2),
116
+ which has specific terms regarding commercial use and distribution. By enabling Phoenix, you acknowledge
117
+ that you are responsible for ensuring compliance with the ELv2 license terms for your specific use case.
118
+ Please review the Phoenix license (https://github.com/Arize-ai/phoenix/blob/main/LICENSE) before enabling
119
+ this feature in production environments.
120
+ """),
121
+ style="dim",
122
+ )
123
+
124
+ await agentstack_cli.commands.server.server_login("http://localhost:8333")
125
+
126
+
127
+ @app.command("stop")
128
+ async def stop(
129
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
130
+ verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
131
+ ):
132
+ """Stop Agent Stack platform."""
133
+ with verbosity(verbose):
134
+ driver = get_driver(vm_name=vm_name)
135
+ if not await driver.status():
136
+ console.info("Agent Stack platform not found. Nothing to stop.")
137
+ return
138
+ await driver.stop()
139
+ console.success("Agent Stack platform stopped successfully.")
140
+
141
+
142
+ @app.command("delete")
143
+ async def delete(
144
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
145
+ verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
146
+ ):
147
+ """Delete Agent Stack platform."""
148
+ with verbosity(verbose):
149
+ driver = get_driver(vm_name=vm_name)
150
+ await driver.delete()
151
+ console.success("Agent Stack platform deleted successfully.")
152
+
153
+
154
+ @app.command("import")
155
+ async def import_image_cmd(
156
+ tag: typing.Annotated[str, typer.Argument(help="Docker image tag to import")],
157
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
158
+ verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
159
+ ):
160
+ """Import a local docker image into the Agent Stack platform."""
161
+ with verbosity(verbose):
162
+ driver = get_driver(vm_name=vm_name)
163
+ if (await driver.status()) != "running":
164
+ console.error("Agent Stack platform is not running.")
165
+ sys.exit(1)
166
+ await driver.import_image(tag)
167
+
168
+
169
+ @app.command("exec")
170
+ async def exec_cmd(
171
+ command: typing.Annotated[list[str] | None, typer.Argument()] = None,
172
+ vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
173
+ verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
174
+ ):
175
+ """For debugging -- execute a command inside the Agent Stack platform VM."""
176
+ with verbosity(verbose, show_success_status=False):
177
+ driver = get_driver(vm_name=vm_name)
178
+ if (await driver.status()) != "running":
179
+ console.error("Agent Stack platform is not running.")
180
+ sys.exit(1)
181
+ await driver.exec(command or ["/bin/bash"])
@@ -0,0 +1,222 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import abc
5
+ import importlib.resources
6
+ import pathlib
7
+ import shlex
8
+ import typing
9
+ from subprocess import CompletedProcess
10
+ from textwrap import dedent
11
+
12
+ import anyio
13
+ import pydantic
14
+ import yaml
15
+ from tenacity import AsyncRetrying, stop_after_attempt
16
+
17
+ import agentstack_cli.commands.platform.istio
18
+ from agentstack_cli.configuration import Configuration
19
+
20
+
21
+ class BaseDriver(abc.ABC):
22
+ vm_name: str
23
+
24
+ def __init__(self, vm_name: str = "agentstack"):
25
+ self.vm_name = vm_name
26
+
27
+ @abc.abstractmethod
28
+ async def run_in_vm(
29
+ self,
30
+ command: list[str],
31
+ message: str,
32
+ env: dict[str, str] | None = None,
33
+ input: bytes | None = None,
34
+ ) -> CompletedProcess[bytes]: ...
35
+
36
+ @abc.abstractmethod
37
+ async def status(self) -> typing.Literal["running"] | str | None: ...
38
+
39
+ @abc.abstractmethod
40
+ async def create_vm(self) -> None: ...
41
+
42
+ @abc.abstractmethod
43
+ async def stop(self) -> None: ...
44
+
45
+ @abc.abstractmethod
46
+ async def delete(self) -> None: ...
47
+
48
+ @abc.abstractmethod
49
+ async def import_image(self, tag: str) -> None: ...
50
+
51
+ @abc.abstractmethod
52
+ async def exec(self, command: list[str]) -> None: ...
53
+
54
+ async def install_tools(self) -> None:
55
+ # Configure k3s registry for local registry access
56
+ registry_config = dedent(
57
+ """\
58
+ mirrors:
59
+ "agentstack-registry-svc.default:5001":
60
+ endpoint:
61
+ - "http://localhost:30501"
62
+ configs:
63
+ "agentstack-registry-svc.default:5001":
64
+ tls:
65
+ insecure_skip_verify: true
66
+ """
67
+ )
68
+
69
+ await self.run_in_vm(
70
+ [
71
+ "sh",
72
+ "-c",
73
+ (
74
+ f"sudo mkdir -p /etc/rancher/k3s /registry-data && "
75
+ f"echo '{registry_config}' | "
76
+ "sudo tee /etc/rancher/k3s/registries.yaml > /dev/null"
77
+ ),
78
+ ],
79
+ "Configuring k3s registry",
80
+ )
81
+
82
+ await self.run_in_vm(
83
+ [
84
+ "sh",
85
+ "-c",
86
+ "which k3s || curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644 --https-listen-port=16443",
87
+ ],
88
+ "Installing k3s",
89
+ )
90
+ await self.run_in_vm(
91
+ [
92
+ "sh",
93
+ "-c",
94
+ "which helm || curl -sfL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash",
95
+ ],
96
+ "Installing Helm",
97
+ )
98
+
99
+ async def deploy(
100
+ self,
101
+ set_values_list: list[str],
102
+ values_file: pathlib.Path | None = None,
103
+ import_images: list[str] | None = None,
104
+ ) -> None:
105
+ await self.run_in_vm(
106
+ ["sh", "-c", "mkdir -p /tmp/agentstack && cat >/tmp/agentstack/chart.tgz"],
107
+ "Preparing Helm chart",
108
+ input=(importlib.resources.files("agentstack_cli") / "data" / "helm-chart.tgz").read_bytes(),
109
+ )
110
+ values = {
111
+ **{svc: {"service": {"type": "LoadBalancer"}} for svc in ["collector", "docling", "ui", "phoenix"]},
112
+ "hostNetwork": True,
113
+ "externalRegistries": {"public_github": str(Configuration().agent_registry)},
114
+ "encryptionKey": "Ovx8qImylfooq4-HNwOzKKDcXLZCB3c_m0JlB9eJBxc=",
115
+ "features": {
116
+ "uiNavigation": True,
117
+ "selfRegistration": True,
118
+ "localSetup": True,
119
+ "generateConversationTitle": False, # TODO: enable when UI implementation is ready
120
+ },
121
+ "auth": {"enabled": False},
122
+ }
123
+ if values_file:
124
+ values.update(yaml.safe_load(values_file.read_text()))
125
+ await self.run_in_vm(
126
+ ["sh", "-c", "cat >/tmp/agentstack/values.yaml"],
127
+ "Preparing Helm values",
128
+ input=yaml.dump(values).encode("utf-8"),
129
+ )
130
+
131
+ images_str = (
132
+ await self.run_in_vm(
133
+ [
134
+ "/bin/bash",
135
+ "-c",
136
+ "helm template agentstack /tmp/agentstack/chart.tgz --values=/tmp/agentstack/values.yaml "
137
+ + " ".join(shlex.quote(f"--set={value}") for value in set_values_list)
138
+ + " | sed -n '/^\\s*image:/{ /{{/!{ s/.*image:\\s*//p } }'",
139
+ ],
140
+ "Listing necessary images",
141
+ )
142
+ ).stdout.decode()
143
+ for image in import_images or []:
144
+ await self.import_image(image)
145
+ for image in {typing.cast(str, yaml.safe_load(line)) for line in images_str.splitlines()} - set(
146
+ import_images or []
147
+ ):
148
+ async for attempt in AsyncRetrying(stop=stop_after_attempt(5)):
149
+ with attempt:
150
+ attempt_num = attempt.retry_state.attempt_number
151
+ await self.run_in_vm(
152
+ [
153
+ "k3s",
154
+ "ctr",
155
+ "image",
156
+ "pull",
157
+ image if "." in image.split("/")[0] else f"docker.io/{image}",
158
+ ],
159
+ f"Pulling image {image}" + (f" (attempt {attempt_num})" if attempt_num > 1 else ""),
160
+ )
161
+
162
+ if any("auth.oidc.enabled=true" in value.lower() for value in set_values_list):
163
+ await agentstack_cli.commands.platform.istio.install(driver=self)
164
+
165
+ kubeconfig_path = anyio.Path(Configuration().lima_home) / self.vm_name / "copied-from-guest" / "kubeconfig.yaml"
166
+ await kubeconfig_path.parent.mkdir(parents=True, exist_ok=True)
167
+ await kubeconfig_path.write_text(
168
+ (
169
+ await self.run_in_vm(
170
+ ["/bin/cat", "/etc/rancher/k3s/k3s.yaml"],
171
+ "Copying kubeconfig from Agent Stack platform",
172
+ )
173
+ ).stdout.decode()
174
+ )
175
+
176
+ await self.run_in_vm(
177
+ [
178
+ "helm",
179
+ "upgrade",
180
+ "--install",
181
+ "agentstack",
182
+ "/tmp/agentstack/chart.tgz",
183
+ "--namespace=default",
184
+ "--create-namespace",
185
+ "--values=/tmp/agentstack/values.yaml",
186
+ "--timeout=20m",
187
+ "--wait",
188
+ "--kubeconfig=/etc/rancher/k3s/k3s.yaml",
189
+ *(f"--set={value}" for value in set_values_list),
190
+ ],
191
+ "Deploying Agent Stack platform with Helm",
192
+ )
193
+
194
+ if import_images:
195
+ await self.run_in_vm(
196
+ ["k3s", "kubectl", "rollout", "restart", "deployment"],
197
+ "Restarting deployments to load imported images",
198
+ )
199
+
200
+ async def version(self) -> str | None:
201
+ if (await self.status()) != "running":
202
+ return None
203
+ HelmStatus = typing.TypedDict("HelmStatus", {"status": str, "app_version": str})
204
+ helm_status = pydantic.TypeAdapter(list[HelmStatus]).validate_json(
205
+ (
206
+ await self.run_in_vm(
207
+ [
208
+ "/usr/local/bin/helm",
209
+ "--kubeconfig=/etc/rancher/k3s/k3s.yaml",
210
+ "ls",
211
+ "--namespace=default",
212
+ "--filter=^agentstack$",
213
+ "-o",
214
+ "json",
215
+ ],
216
+ "Getting Agent Stack platform version",
217
+ )
218
+ ).stdout
219
+ )
220
+ if helm_status[0]["status"] != "deployed":
221
+ return None
222
+ return helm_status[0]["app_version"]
@@ -0,0 +1,186 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import typing
5
+
6
+ import yaml
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from agentstack_cli.commands.platform.base_driver import BaseDriver
10
+
11
+
12
+ async def install(driver: "BaseDriver"):
13
+ # Gateway API
14
+ await driver.run_in_vm(
15
+ [
16
+ "k3s",
17
+ "kubectl",
18
+ "apply",
19
+ "-f",
20
+ "https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml",
21
+ ],
22
+ "Installing gateway CRDs",
23
+ )
24
+
25
+ # Cert Manager
26
+ await driver.run_in_vm(
27
+ [
28
+ "helm",
29
+ "--kubeconfig=/etc/rancher/k3s/k3s.yaml",
30
+ "install",
31
+ "cert-manager",
32
+ "oci://quay.io/jetstack/charts/cert-manager",
33
+ "--version",
34
+ "v1.18.2",
35
+ "--namespace",
36
+ "cert-manager",
37
+ "--create-namespace",
38
+ "--set",
39
+ "crds.enabled=true",
40
+ "--wait",
41
+ ],
42
+ "Installing cert-manager",
43
+ )
44
+
45
+ # Istio
46
+ await driver.run_in_vm(
47
+ ["helm", "repo", "add", "istio", "https://istio-release.storage.googleapis.com/charts"],
48
+ "Adding Istio repo to Helm",
49
+ )
50
+ await driver.run_in_vm(["helm", "repo", "update"], "Updating Helm repos")
51
+ for component in ["base", "istiod", "cni", "ztunnel"]:
52
+ await driver.run_in_vm(
53
+ [
54
+ "helm",
55
+ "--kubeconfig=/etc/rancher/k3s/k3s.yaml",
56
+ "install",
57
+ f"istio-{component}",
58
+ f"istio/{component}",
59
+ "--namespace",
60
+ "istio-system",
61
+ "--create-namespace",
62
+ "--set=profile=ambient",
63
+ "--set=global.platform=k3s",
64
+ "--wait",
65
+ ],
66
+ f"Installing Istio ({component})",
67
+ )
68
+ await driver.run_in_vm(
69
+ ["k3s", "kubectl", "label", "namespace", "default", "istio.io/dataplane-mode=ambient"],
70
+ "Labeling the default namespace",
71
+ )
72
+
73
+ # Configuration
74
+ Resource = typing.TypedDict(
75
+ "Resource", {"apiVersion": str, "kind": str, "metadata": dict[str, str], "spec": dict[str, typing.Any]}
76
+ )
77
+ resources: list[Resource] = [
78
+ {
79
+ "apiVersion": "cert-manager.io/v1",
80
+ "kind": "Issuer",
81
+ "metadata": {"name": "default-issuer", "namespace": "default"},
82
+ "spec": {"selfSigned": {}},
83
+ },
84
+ {
85
+ "apiVersion": "cert-manager.io/v1",
86
+ "kind": "Issuer",
87
+ "metadata": {"name": "istio-system-issuer", "namespace": "istio-system"},
88
+ "spec": {"selfSigned": {}},
89
+ },
90
+ {
91
+ "apiVersion": "cert-manager.io/v1",
92
+ "kind": "Certificate",
93
+ "metadata": {"name": "agentstack-tls", "namespace": "istio-system"},
94
+ "spec": {
95
+ "secretName": "agentstack-tls",
96
+ "commonName": "agentstack",
97
+ "dnsNames": ["agentstack", "agentstack.localhost"],
98
+ "issuerRef": {"name": "istio-system-issuer", "kind": "Issuer"},
99
+ },
100
+ },
101
+ {
102
+ "apiVersion": "cert-manager.io/v1",
103
+ "kind": "Certificate",
104
+ "metadata": {"name": "ingestion-svc", "namespace": "default"},
105
+ "spec": {
106
+ "secretName": "ingestion-svc-tls",
107
+ "commonName": "ingestion-svc",
108
+ "dnsNames": [
109
+ "ingestion-svc",
110
+ "ingestion-svc.default",
111
+ "ingestion-svc.default.svc",
112
+ "ingestion-svc.default.svc.cluster.local",
113
+ ],
114
+ "issuerRef": {"name": "default-issuer", "kind": "Issuer"},
115
+ },
116
+ },
117
+ {
118
+ "apiVersion": "gateway.networking.k8s.io/v1",
119
+ "kind": "Gateway",
120
+ "metadata": {"name": "agentstack-gateway", "namespace": "istio-system"},
121
+ "spec": {
122
+ "gatewayClassName": "istio",
123
+ "listeners": [
124
+ {
125
+ "name": "https",
126
+ "hostname": "agentstack.localhost",
127
+ "port": 8336,
128
+ "protocol": "HTTPS",
129
+ "tls": {"mode": "Terminate", "certificateRefs": [{"name": "agentstack-tls"}]},
130
+ "allowedRoutes": {"namespaces": {"from": "All"}},
131
+ }
132
+ ],
133
+ },
134
+ },
135
+ {
136
+ "apiVersion": "gateway.networking.k8s.io/v1",
137
+ "kind": "HTTPRoute",
138
+ "metadata": {"name": "agentstack-ui"},
139
+ "spec": {
140
+ "parentRefs": [{"name": "agentstack-gateway", "namespace": "istio-system"}],
141
+ "hostnames": ["agentstack.testing", "agentstack.localhost"],
142
+ "rules": [
143
+ {
144
+ "matches": [{"path": {"type": "PathPrefix", "value": "/"}}],
145
+ "backendRefs": [{"name": "agentstack-ui-svc", "port": 8334}],
146
+ }
147
+ ],
148
+ },
149
+ },
150
+ ]
151
+ for resource in resources:
152
+ await driver.run_in_vm(
153
+ ["k3s", "kubectl", "apply", "-f", "-"],
154
+ f"Applying {resource['metadata']['name']} ({resource['kind']})",
155
+ input=yaml.dump(resource, sort_keys=False).encode("utf-8"),
156
+ )
157
+
158
+ # Extra services
159
+ for addon in ["prometheus", "kiali"]:
160
+ await driver.run_in_vm(
161
+ [
162
+ "k3s",
163
+ "kubectl",
164
+ "apply",
165
+ "-f",
166
+ f"https://raw.githubusercontent.com/istio/istio/master/samples/addons/{addon}.yaml",
167
+ ],
168
+ f"Installing {addon.capitalize()}",
169
+ )
170
+ await driver.run_in_vm(
171
+ [
172
+ "k3s",
173
+ "kubectl",
174
+ "-n",
175
+ "istio-system",
176
+ "expose",
177
+ "deployment",
178
+ "kiali",
179
+ "--protocol=TCP",
180
+ "--port=20001",
181
+ "--target-port=20001",
182
+ "--type=LoadBalancer",
183
+ "--name=kiali-external",
184
+ ],
185
+ "Exposing Kiali service",
186
+ )