vystak-provider-docker 0.1.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.
- vystak_provider_docker-0.1.0/.gitignore +40 -0
- vystak_provider_docker-0.1.0/PKG-INFO +53 -0
- vystak_provider_docker-0.1.0/README.md +27 -0
- vystak_provider_docker-0.1.0/pyproject.toml +43 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/__init__.py +7 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/gateway.py +121 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/network.py +13 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/nodes/__init__.py +13 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/nodes/agent.py +182 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/nodes/gateway.py +84 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/nodes/network.py +40 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/nodes/service.py +155 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/provider.py +315 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/resources.py +145 -0
- vystak_provider_docker-0.1.0/src/vystak_provider_docker/secrets.py +36 -0
- vystak_provider_docker-0.1.0/tests/test_gateway_provision.py +125 -0
- vystak_provider_docker-0.1.0/tests/test_network.py +30 -0
- vystak_provider_docker-0.1.0/tests/test_nodes.py +233 -0
- vystak_provider_docker-0.1.0/tests/test_provider.py +234 -0
- vystak_provider_docker-0.1.0/tests/test_resources.py +126 -0
- vystak_provider_docker-0.1.0/tests/test_secrets.py +55 -0
- vystak_provider_docker-0.1.0/tests/test_version.py +5 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
*.egg
|
|
10
|
+
.venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.pyright/
|
|
14
|
+
|
|
15
|
+
# TypeScript
|
|
16
|
+
node_modules/
|
|
17
|
+
*.tsbuildinfo
|
|
18
|
+
coverage/
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
*~
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Environment
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
|
34
|
+
|
|
35
|
+
# Vystak generated output (was .agentstack/ before rename)
|
|
36
|
+
.vystak/
|
|
37
|
+
.agentstack/
|
|
38
|
+
|
|
39
|
+
# Tooling workspaces (brainstorm sessions, local agent state)
|
|
40
|
+
.superpowers/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vystak-provider-docker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vystak platform provider — deploys agents to Docker
|
|
5
|
+
Project-URL: Homepage, https://vystak.dev
|
|
6
|
+
Project-URL: Documentation, https://vystak.dev/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/vystak/vystak
|
|
8
|
+
Project-URL: Issues, https://github.com/vystak/vystak/issues
|
|
9
|
+
Author-email: Anatoliy Kolodkin <11351966+akolodkin@users.noreply.github.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: agent,ai,deployment,docker,provider,vystak
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: docker>=7.0
|
|
24
|
+
Requires-Dist: vystak>=0.1.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# vystak-provider-docker
|
|
28
|
+
|
|
29
|
+
Platform provider that deploys Vystak agents to Docker.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install vystak-provider-docker
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Usually you don't install this directly — [`vystak-cli`](https://pypi.org/project/vystak-cli/) depends on it and invokes it automatically when your agent's `platform.type` is `docker`.
|
|
38
|
+
|
|
39
|
+
## What it does
|
|
40
|
+
|
|
41
|
+
On `vystak apply`, the provider builds a `ProvisionGraph` and rolls out, in topological order:
|
|
42
|
+
|
|
43
|
+
- **Docker network** (`vystak-net`) — shared bridge for inter-agent A2A calls
|
|
44
|
+
- **Postgres containers** — managed session / memory stores (if declared)
|
|
45
|
+
- **SQLite volumes** — persistent local-only alternative
|
|
46
|
+
- **Agent container** — builds the image from generated code, runs it on the shared network
|
|
47
|
+
- **Gateway container** — optional; routes Slack / OpenAI-compatible traffic to one or more agents
|
|
48
|
+
|
|
49
|
+
Each node has a health check; the provider waits for each to report healthy before proceeding to dependents.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
Apache-2.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# vystak-provider-docker
|
|
2
|
+
|
|
3
|
+
Platform provider that deploys Vystak agents to Docker.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install vystak-provider-docker
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Usually you don't install this directly — [`vystak-cli`](https://pypi.org/project/vystak-cli/) depends on it and invokes it automatically when your agent's `platform.type` is `docker`.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
On `vystak apply`, the provider builds a `ProvisionGraph` and rolls out, in topological order:
|
|
16
|
+
|
|
17
|
+
- **Docker network** (`vystak-net`) — shared bridge for inter-agent A2A calls
|
|
18
|
+
- **Postgres containers** — managed session / memory stores (if declared)
|
|
19
|
+
- **SQLite volumes** — persistent local-only alternative
|
|
20
|
+
- **Agent container** — builds the image from generated code, runs it on the shared network
|
|
21
|
+
- **Gateway container** — optional; routes Slack / OpenAI-compatible traffic to one or more agents
|
|
22
|
+
|
|
23
|
+
Each node has a health check; the provider waits for each to report healthy before proceeding to dependents.
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
Apache-2.0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vystak-provider-docker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Vystak platform provider — deploys agents to Docker"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Anatoliy Kolodkin", email = "11351966+akolodkin@users.noreply.github.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["vystak", "docker", "agent", "ai", "deployment", "provider"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: System :: Systems Administration",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"vystak>=0.1.0",
|
|
26
|
+
"docker>=7.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://vystak.dev"
|
|
31
|
+
Documentation = "https://vystak.dev/docs"
|
|
32
|
+
Repository = "https://github.com/vystak/vystak"
|
|
33
|
+
Issues = "https://github.com/vystak/vystak/issues"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/vystak_provider_docker"]
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources]
|
|
43
|
+
vystak = { workspace = true }
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Gateway container provisioning for Docker."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import docker
|
|
7
|
+
import docker.errors
|
|
8
|
+
|
|
9
|
+
GATEWAY_DOCKERFILE = """\
|
|
10
|
+
FROM python:3.11-slim
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
COPY requirements.txt .
|
|
13
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
14
|
+
COPY . .
|
|
15
|
+
CMD ["python", "server.py"]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
GATEWAY_REQUIREMENTS = """\
|
|
19
|
+
fastapi>=0.115
|
|
20
|
+
uvicorn>=0.34
|
|
21
|
+
httpx>=0.28
|
|
22
|
+
slack-bolt>=1.21
|
|
23
|
+
aiohttp>=3.9
|
|
24
|
+
aiosqlite>=0.20
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _gateway_container_name(gateway_name: str) -> str:
|
|
29
|
+
return f"vystak-gateway-{gateway_name}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_gateway_source(gateway_dir: Path) -> None:
|
|
33
|
+
"""Write gateway server source files to a build directory."""
|
|
34
|
+
gateway_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
import vystak_gateway
|
|
37
|
+
|
|
38
|
+
pkg_dir = Path(vystak_gateway.__file__).parent
|
|
39
|
+
|
|
40
|
+
for filename in ["server.py", "router.py", "store.py", "__init__.py"]:
|
|
41
|
+
src = pkg_dir / filename
|
|
42
|
+
if src.exists():
|
|
43
|
+
content = src.read_text()
|
|
44
|
+
# Rewrite package imports to local imports for Docker deployment
|
|
45
|
+
content = content.replace("from vystak_gateway.", "from ")
|
|
46
|
+
content = content.replace(
|
|
47
|
+
"from vystak.schema.openai import", "from openai_types import"
|
|
48
|
+
)
|
|
49
|
+
(gateway_dir / filename).write_text(content)
|
|
50
|
+
|
|
51
|
+
# Bundle OpenAI-compatible schema types
|
|
52
|
+
import vystak.schema.openai as _openai_schema
|
|
53
|
+
|
|
54
|
+
_openai_src = Path(_openai_schema.__file__)
|
|
55
|
+
if _openai_src.exists():
|
|
56
|
+
(gateway_dir / "openai_types.py").write_text(_openai_src.read_text())
|
|
57
|
+
|
|
58
|
+
providers_dir = gateway_dir / "providers"
|
|
59
|
+
providers_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
providers_src = pkg_dir / "providers"
|
|
61
|
+
for filename in ["__init__.py", "base.py", "slack.py"]:
|
|
62
|
+
src = providers_src / filename
|
|
63
|
+
if src.exists():
|
|
64
|
+
content = src.read_text()
|
|
65
|
+
content = content.replace("from vystak_gateway.", "from ")
|
|
66
|
+
(providers_dir / filename).write_text(content)
|
|
67
|
+
|
|
68
|
+
(gateway_dir / "requirements.txt").write_text(GATEWAY_REQUIREMENTS)
|
|
69
|
+
(gateway_dir / "Dockerfile").write_text(GATEWAY_DOCKERFILE)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_routes_file(routes_path: Path, providers_list: list, routes_list: list) -> None:
|
|
73
|
+
"""Write routes.json for the gateway."""
|
|
74
|
+
routes_path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
data = {"providers": providers_list, "routes": routes_list}
|
|
76
|
+
routes_path.write_text(json.dumps(data, indent=2))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_gateway_image(client, gateway_name: str, gateway_dir: str) -> None:
|
|
80
|
+
"""Build a Docker image for the gateway."""
|
|
81
|
+
image_tag = f"{_gateway_container_name(gateway_name)}:latest"
|
|
82
|
+
client.images.build(path=gateway_dir, tag=image_tag)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def provision_gateway(
|
|
86
|
+
client, gateway_name: str, network, routes_path: str, env: dict, port: int = 8080
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Start or restart a gateway container."""
|
|
89
|
+
container_name = _gateway_container_name(gateway_name)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
existing = client.containers.get(container_name)
|
|
93
|
+
existing.stop()
|
|
94
|
+
existing.remove()
|
|
95
|
+
except docker.errors.NotFound:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
image_tag = f"{container_name}:latest"
|
|
99
|
+
abs_routes = str(Path(routes_path).resolve())
|
|
100
|
+
|
|
101
|
+
client.containers.run(
|
|
102
|
+
image_tag,
|
|
103
|
+
name=container_name,
|
|
104
|
+
detach=True,
|
|
105
|
+
ports={"8080/tcp": port},
|
|
106
|
+
environment=env,
|
|
107
|
+
volumes={abs_routes: {"bind": "/app/routes.json", "mode": "ro"}},
|
|
108
|
+
network=network.name,
|
|
109
|
+
labels={"vystak.gateway": gateway_name},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def destroy_gateway(client, gateway_name: str) -> None:
|
|
114
|
+
"""Stop and remove a gateway container."""
|
|
115
|
+
container_name = _gateway_container_name(gateway_name)
|
|
116
|
+
try:
|
|
117
|
+
container = client.containers.get(container_name)
|
|
118
|
+
container.stop()
|
|
119
|
+
container.remove()
|
|
120
|
+
except docker.errors.NotFound:
|
|
121
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Docker network management for Vystak."""
|
|
2
|
+
|
|
3
|
+
import docker # noqa: F401 — re-exported for test patching (vystak_provider_docker.network.docker)
|
|
4
|
+
|
|
5
|
+
NETWORK_NAME = "vystak-net"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ensure_network(client, name: str = NETWORK_NAME):
|
|
9
|
+
"""Create the Vystak Docker network if it doesn't exist."""
|
|
10
|
+
existing = client.networks.list(names=[name])
|
|
11
|
+
if existing:
|
|
12
|
+
return existing[0]
|
|
13
|
+
return client.networks.create(name, driver="bridge")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Docker provider node types for the provisioning graph."""
|
|
2
|
+
|
|
3
|
+
from vystak_provider_docker.nodes.agent import DockerAgentNode
|
|
4
|
+
from vystak_provider_docker.nodes.gateway import DockerGatewayNode
|
|
5
|
+
from vystak_provider_docker.nodes.network import DockerNetworkNode
|
|
6
|
+
from vystak_provider_docker.nodes.service import DockerServiceNode
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"DockerAgentNode",
|
|
10
|
+
"DockerGatewayNode",
|
|
11
|
+
"DockerNetworkNode",
|
|
12
|
+
"DockerServiceNode",
|
|
13
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""DockerAgentNode — builds and runs an agent as a Docker container."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import docker.errors
|
|
7
|
+
from vystak.providers.base import DeployPlan, GeneratedCode
|
|
8
|
+
from vystak.provisioning.health import HealthCheck, NoopHealthCheck
|
|
9
|
+
from vystak.provisioning.node import Provisionable, ProvisionResult
|
|
10
|
+
from vystak.schema.agent import Agent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DockerAgentNode(Provisionable):
|
|
14
|
+
"""Builds a Docker image and runs an agent container."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client, agent: Agent, generated_code: GeneratedCode, plan: DeployPlan):
|
|
17
|
+
self._client = client
|
|
18
|
+
self._agent = agent
|
|
19
|
+
self._generated_code = generated_code
|
|
20
|
+
self._plan = plan
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return f"agent:{self._agent.name}"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def depends_on(self) -> list[str]:
|
|
28
|
+
deps = ["network"]
|
|
29
|
+
if self._agent.sessions is not None:
|
|
30
|
+
deps.append(self._agent.sessions.name)
|
|
31
|
+
if self._agent.memory is not None:
|
|
32
|
+
deps.append(self._agent.memory.name)
|
|
33
|
+
for svc in self._agent.services:
|
|
34
|
+
deps.append(svc.name)
|
|
35
|
+
return deps
|
|
36
|
+
|
|
37
|
+
def _container_name(self) -> str:
|
|
38
|
+
return f"vystak-{self._agent.name}"
|
|
39
|
+
|
|
40
|
+
def provision(self, context: dict) -> ProvisionResult:
|
|
41
|
+
try:
|
|
42
|
+
container_name = self._container_name()
|
|
43
|
+
network = context["network"].info["network"]
|
|
44
|
+
|
|
45
|
+
# Stop existing container
|
|
46
|
+
try:
|
|
47
|
+
existing = self._client.containers.get(container_name)
|
|
48
|
+
existing.stop()
|
|
49
|
+
existing.remove()
|
|
50
|
+
except docker.errors.NotFound:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# Write build files
|
|
54
|
+
build_dir = Path(".vystak") / self._agent.name
|
|
55
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
for filename, content in self._generated_code.files.items():
|
|
57
|
+
file_path = build_dir / filename
|
|
58
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
file_path.write_text(content)
|
|
60
|
+
|
|
61
|
+
# Bundle OpenAI-compatible schema types for Docker deployment
|
|
62
|
+
import vystak.schema.openai as _openai_schema
|
|
63
|
+
|
|
64
|
+
_openai_src = Path(_openai_schema.__file__)
|
|
65
|
+
if _openai_src.exists():
|
|
66
|
+
(build_dir / "openai_types.py").write_text(_openai_src.read_text())
|
|
67
|
+
|
|
68
|
+
# Build Dockerfile
|
|
69
|
+
mcp_installs = ""
|
|
70
|
+
needs_node = False
|
|
71
|
+
if self._agent.mcp_servers:
|
|
72
|
+
install_cmds = []
|
|
73
|
+
for mcp in self._agent.mcp_servers:
|
|
74
|
+
if mcp.install:
|
|
75
|
+
install_cmds.append(f"RUN {mcp.install}")
|
|
76
|
+
for field in (mcp.install or "", mcp.command or ""):
|
|
77
|
+
if "npm" in field or "npx" in field:
|
|
78
|
+
needs_node = True
|
|
79
|
+
if install_cmds:
|
|
80
|
+
mcp_installs = "\n".join(install_cmds) + "\n"
|
|
81
|
+
|
|
82
|
+
node_install = ""
|
|
83
|
+
if needs_node:
|
|
84
|
+
node_install = (
|
|
85
|
+
"RUN apt-get update && apt-get install -y nodejs npm "
|
|
86
|
+
"&& rm -rf /var/lib/apt/lists/*\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
dockerfile_content = (
|
|
90
|
+
"FROM python:3.11-slim\n"
|
|
91
|
+
"WORKDIR /app\n"
|
|
92
|
+
f"{node_install}"
|
|
93
|
+
f"{mcp_installs}"
|
|
94
|
+
"COPY requirements.txt .\n"
|
|
95
|
+
"RUN pip install --no-cache-dir -r requirements.txt\n"
|
|
96
|
+
"COPY . .\n"
|
|
97
|
+
f'CMD ["python", "{self._generated_code.entrypoint}"]\n'
|
|
98
|
+
)
|
|
99
|
+
(build_dir / "Dockerfile").write_text(dockerfile_content)
|
|
100
|
+
|
|
101
|
+
# Build image
|
|
102
|
+
image_tag = f"{container_name}:latest"
|
|
103
|
+
self._client.images.build(path=str(build_dir), tag=image_tag)
|
|
104
|
+
|
|
105
|
+
# Build env vars
|
|
106
|
+
env = {}
|
|
107
|
+
for secret in self._agent.secrets:
|
|
108
|
+
value = os.environ.get(secret.name)
|
|
109
|
+
if value:
|
|
110
|
+
env[secret.name] = value
|
|
111
|
+
# Connection strings from upstream services
|
|
112
|
+
if self._agent.sessions:
|
|
113
|
+
dep_result = context.get(self._agent.sessions.name)
|
|
114
|
+
if dep_result and dep_result.info.get("connection_string"):
|
|
115
|
+
env["SESSION_STORE_URL"] = dep_result.info["connection_string"]
|
|
116
|
+
|
|
117
|
+
if self._agent.memory:
|
|
118
|
+
dep_result = context.get(self._agent.memory.name)
|
|
119
|
+
if dep_result and dep_result.info.get("connection_string"):
|
|
120
|
+
env["MEMORY_STORE_URL"] = dep_result.info["connection_string"]
|
|
121
|
+
|
|
122
|
+
# Build volumes
|
|
123
|
+
volumes = {}
|
|
124
|
+
for dep_name in self.depends_on:
|
|
125
|
+
if dep_name == "network":
|
|
126
|
+
continue
|
|
127
|
+
dep_result = context.get(dep_name)
|
|
128
|
+
if dep_result and dep_result.info.get("engine") == "sqlite":
|
|
129
|
+
volumes[dep_result.info["volume_name"]] = {
|
|
130
|
+
"bind": "/data",
|
|
131
|
+
"mode": "rw",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Run container
|
|
135
|
+
host_port = self._agent.port if self._agent.port else None
|
|
136
|
+
self._client.containers.run(
|
|
137
|
+
image_tag,
|
|
138
|
+
name=container_name,
|
|
139
|
+
detach=True,
|
|
140
|
+
ports={"8000/tcp": host_port},
|
|
141
|
+
environment=env,
|
|
142
|
+
volumes=volumes,
|
|
143
|
+
network=network.name,
|
|
144
|
+
labels={
|
|
145
|
+
"vystak.hash": self._plan.target_hash,
|
|
146
|
+
"vystak.agent": self._agent.name,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Get the actual port
|
|
151
|
+
container = self._client.containers.get(container_name)
|
|
152
|
+
port_info = container.ports.get("8000/tcp")
|
|
153
|
+
actual_port = port_info[0]["HostPort"] if port_info else "?"
|
|
154
|
+
url = f"http://localhost:{actual_port}"
|
|
155
|
+
|
|
156
|
+
return ProvisionResult(
|
|
157
|
+
name=self.name,
|
|
158
|
+
success=True,
|
|
159
|
+
info={
|
|
160
|
+
"url": url,
|
|
161
|
+
"container_name": container_name,
|
|
162
|
+
"port": actual_port,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return ProvisionResult(
|
|
167
|
+
name=self.name,
|
|
168
|
+
success=False,
|
|
169
|
+
error=str(e),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def health_check(self) -> HealthCheck:
|
|
173
|
+
return NoopHealthCheck()
|
|
174
|
+
|
|
175
|
+
def destroy(self) -> None:
|
|
176
|
+
container_name = self._container_name()
|
|
177
|
+
try:
|
|
178
|
+
container = self._client.containers.get(container_name)
|
|
179
|
+
container.stop()
|
|
180
|
+
container.remove()
|
|
181
|
+
except docker.errors.NotFound:
|
|
182
|
+
pass
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""DockerGatewayNode — provisions a gateway container."""
|
|
2
|
+
|
|
3
|
+
from vystak.provisioning.health import HealthCheck, NoopHealthCheck
|
|
4
|
+
from vystak.provisioning.node import Provisionable, ProvisionResult
|
|
5
|
+
|
|
6
|
+
from vystak_provider_docker.gateway import (
|
|
7
|
+
build_gateway_image,
|
|
8
|
+
destroy_gateway,
|
|
9
|
+
provision_gateway,
|
|
10
|
+
write_gateway_source,
|
|
11
|
+
write_routes_file,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DockerGatewayNode(Provisionable):
|
|
16
|
+
"""Provisions a gateway container for agent channel routing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client, gw_name: str, gw_info: dict, agent_name: str):
|
|
19
|
+
self._client = client
|
|
20
|
+
self._gw_name = gw_name
|
|
21
|
+
self._gw_info = gw_info
|
|
22
|
+
self._agent_name = agent_name
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
return f"gateway:{self._gw_name}"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def depends_on(self) -> list[str]:
|
|
30
|
+
return [f"agent:{self._agent_name}"]
|
|
31
|
+
|
|
32
|
+
def provision(self, context: dict) -> ProvisionResult:
|
|
33
|
+
try:
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
network = context["network"].info["network"]
|
|
37
|
+
gateway = self._gw_info.get("gateway")
|
|
38
|
+
gateway_dir = Path(".vystak") / f"gateway-{self._gw_name}"
|
|
39
|
+
|
|
40
|
+
write_gateway_source(gateway_dir)
|
|
41
|
+
|
|
42
|
+
routes_path = gateway_dir / "routes.json"
|
|
43
|
+
write_routes_file(
|
|
44
|
+
routes_path,
|
|
45
|
+
list(self._gw_info.get("providers", {}).values()),
|
|
46
|
+
self._gw_info.get("routes", []),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
build_gateway_image(self._client, self._gw_name, str(gateway_dir))
|
|
50
|
+
|
|
51
|
+
env = {}
|
|
52
|
+
for prov in self._gw_info.get("providers", {}).values():
|
|
53
|
+
for key, value in prov.get("config", {}).items():
|
|
54
|
+
if isinstance(value, str) and value:
|
|
55
|
+
env_key = f"{prov['name'].upper().replace('-', '_')}_{key.upper()}"
|
|
56
|
+
env[env_key] = value
|
|
57
|
+
|
|
58
|
+
port = gateway.config.get("port", 8080) if gateway else 8080
|
|
59
|
+
provision_gateway(
|
|
60
|
+
self._client,
|
|
61
|
+
self._gw_name,
|
|
62
|
+
network,
|
|
63
|
+
routes_path=str(routes_path),
|
|
64
|
+
env=env,
|
|
65
|
+
port=port,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return ProvisionResult(
|
|
69
|
+
name=self.name,
|
|
70
|
+
success=True,
|
|
71
|
+
info={"gateway_name": self._gw_name, "port": port},
|
|
72
|
+
)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return ProvisionResult(
|
|
75
|
+
name=self.name,
|
|
76
|
+
success=False,
|
|
77
|
+
error=str(e),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def health_check(self) -> HealthCheck:
|
|
81
|
+
return NoopHealthCheck()
|
|
82
|
+
|
|
83
|
+
def destroy(self) -> None:
|
|
84
|
+
destroy_gateway(self._client, self._gw_name)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""DockerNetworkNode — creates the shared Docker network."""
|
|
2
|
+
|
|
3
|
+
from vystak.provisioning.health import HealthCheck, NoopHealthCheck
|
|
4
|
+
from vystak.provisioning.node import Provisionable, ProvisionResult
|
|
5
|
+
|
|
6
|
+
NETWORK_NAME = "vystak-net"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DockerNetworkNode(Provisionable):
|
|
10
|
+
"""Ensures the vystak-net Docker network exists."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client):
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
return "network"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def depends_on(self) -> list[str]:
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
def provision(self, context: dict) -> ProvisionResult:
|
|
24
|
+
existing = self._client.networks.list(names=[NETWORK_NAME])
|
|
25
|
+
if existing:
|
|
26
|
+
network = existing[0]
|
|
27
|
+
else:
|
|
28
|
+
network = self._client.networks.create(NETWORK_NAME, driver="bridge")
|
|
29
|
+
|
|
30
|
+
return ProvisionResult(
|
|
31
|
+
name=self.name,
|
|
32
|
+
success=True,
|
|
33
|
+
info={"network_name": network.name, "network": network},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def health_check(self) -> HealthCheck:
|
|
37
|
+
return NoopHealthCheck()
|
|
38
|
+
|
|
39
|
+
def destroy(self) -> None:
|
|
40
|
+
pass # Networks persist across deployments
|