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.
@@ -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,7 @@
1
+ """Vystak Docker platform provider."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from vystak_provider_docker.provider import DockerProvider
6
+
7
+ __all__ = ["DockerProvider", "__version__"]
@@ -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