bilrost 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.
- bilrost-0.1.0/.gitignore +6 -0
- bilrost-0.1.0/LICENSE +21 -0
- bilrost-0.1.0/PKG-INFO +119 -0
- bilrost-0.1.0/README.md +80 -0
- bilrost-0.1.0/pyproject.toml +67 -0
- bilrost-0.1.0/src/sandbox_cli/__init__.py +0 -0
- bilrost-0.1.0/src/sandbox_cli/_capture.py +69 -0
- bilrost-0.1.0/src/sandbox_cli/ansible_runner.py +96 -0
- bilrost-0.1.0/src/sandbox_cli/app.py +194 -0
- bilrost-0.1.0/src/sandbox_cli/bootstrap.py +149 -0
- bilrost-0.1.0/src/sandbox_cli/dashboard.py +75 -0
- bilrost-0.1.0/src/sandbox_cli/deps.py +61 -0
- bilrost-0.1.0/src/sandbox_cli/lima_config.py +201 -0
- bilrost-0.1.0/src/sandbox_cli/lima_manager.py +206 -0
- bilrost-0.1.0/src/sandbox_cli/mcp_server.py +365 -0
- bilrost-0.1.0/src/sandbox_cli/models.py +74 -0
- bilrost-0.1.0/src/sandbox_cli/orchestrator.py +181 -0
- bilrost-0.1.0/src/sandbox_cli/profile.py +117 -0
- bilrost-0.1.0/src/sandbox_cli/reporting.py +235 -0
- bilrost-0.1.0/src/sandbox_cli/templates/lima-vm.yaml.j2 +84 -0
- bilrost-0.1.0/src/sandbox_cli/validation.py +117 -0
bilrost-0.1.0/.gitignore
ADDED
bilrost-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Peleke Sengstacke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bilrost-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bilrost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hardened Lima VM for running AI agents — overlay isolation, network containment, secrets management, and gated sync.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Peleke/openclaw-sandbox
|
|
6
|
+
Project-URL: Documentation, https://peleke.github.io/openclaw-sandbox/
|
|
7
|
+
Project-URL: Repository, https://github.com/Peleke/openclaw-sandbox
|
|
8
|
+
Project-URL: Issues, https://github.com/Peleke/openclaw-sandbox/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Peleke/openclaw-sandbox/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Peleke Sengstacke
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agents,isolation,lima,sandbox,security,vm
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Security
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Classifier: Topic :: System :: Emulators
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: fastmcp<3,>=2
|
|
29
|
+
Requires-Dist: jinja2<4,>=3.1
|
|
30
|
+
Requires-Dist: pydantic<3,>=2
|
|
31
|
+
Requires-Dist: rich<14,>=13
|
|
32
|
+
Requires-Dist: tomli-w<2,>=1
|
|
33
|
+
Requires-Dist: tomli<3,>=2; python_version < '3.11'
|
|
34
|
+
Requires-Dist: typer<1,>=0.12
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
37
|
+
Requires-Dist: pyyaml<7,>=6; extra == 'dev'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# bilrost
|
|
41
|
+
|
|
42
|
+
**Hardened Lima VM for running AI agents** — overlay isolation, network containment, secrets management, and gated sync.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Via pipx (recommended)
|
|
48
|
+
pipx install bilrost
|
|
49
|
+
|
|
50
|
+
# Via uv
|
|
51
|
+
uv tool install bilrost
|
|
52
|
+
|
|
53
|
+
# Via pip
|
|
54
|
+
pip install bilrost
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Interactive setup
|
|
61
|
+
bilrost init
|
|
62
|
+
|
|
63
|
+
# Provision the VM (~5 min first run)
|
|
64
|
+
bilrost up
|
|
65
|
+
|
|
66
|
+
# Check status
|
|
67
|
+
bilrost status
|
|
68
|
+
|
|
69
|
+
# SSH into the VM
|
|
70
|
+
bilrost ssh
|
|
71
|
+
|
|
72
|
+
# Sync overlay changes to host (with secret scanning)
|
|
73
|
+
bilrost sync
|
|
74
|
+
|
|
75
|
+
# Stop / destroy
|
|
76
|
+
bilrost down
|
|
77
|
+
bilrost destroy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## MCP Server
|
|
81
|
+
|
|
82
|
+
Agents can manage the sandbox programmatically via FastMCP:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"sandbox": {
|
|
88
|
+
"command": "bilrost-mcp"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
9 tools: `sandbox_status`, `sandbox_up`, `sandbox_down`, `sandbox_destroy`, `sandbox_exec`, `sandbox_validate`, `sandbox_ssh_info`, `sandbox_gateway_info`, `sandbox_agent_identity`.
|
|
95
|
+
|
|
96
|
+
## What It Does
|
|
97
|
+
|
|
98
|
+
- **OverlayFS isolation** — host code mounted read-only, all writes contained in VM overlay
|
|
99
|
+
- **Network containment** — UFW firewall with explicit allowlist (HTTPS, DNS, Tailscale, NTP only)
|
|
100
|
+
- **Secrets management** — three injection methods, `0600` perms, never in process lists
|
|
101
|
+
- **Gated sync** — gitleaks scanning + path allowlist before changes reach your host
|
|
102
|
+
- **Docker sandboxing** — per-session containers with configurable network isolation
|
|
103
|
+
- **12 Ansible roles** — overlay, secrets, gateway, docker, firewall, sync-gate, gh-cli, buildlog, cadence, qortex, tailscale, and more
|
|
104
|
+
|
|
105
|
+
## Requirements
|
|
106
|
+
|
|
107
|
+
- macOS (Apple Silicon or Intel)
|
|
108
|
+
- [Homebrew](https://brew.sh/)
|
|
109
|
+
- ~10GB disk space
|
|
110
|
+
|
|
111
|
+
Dependencies (Lima, Ansible, etc.) are installed automatically on first run.
|
|
112
|
+
|
|
113
|
+
## Documentation
|
|
114
|
+
|
|
115
|
+
Full docs: [peleke.github.io/openclaw-sandbox](https://peleke.github.io/openclaw-sandbox/)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
bilrost-0.1.0/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# bilrost
|
|
2
|
+
|
|
3
|
+
**Hardened Lima VM for running AI agents** — overlay isolation, network containment, secrets management, and gated sync.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Via pipx (recommended)
|
|
9
|
+
pipx install bilrost
|
|
10
|
+
|
|
11
|
+
# Via uv
|
|
12
|
+
uv tool install bilrost
|
|
13
|
+
|
|
14
|
+
# Via pip
|
|
15
|
+
pip install bilrost
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Interactive setup
|
|
22
|
+
bilrost init
|
|
23
|
+
|
|
24
|
+
# Provision the VM (~5 min first run)
|
|
25
|
+
bilrost up
|
|
26
|
+
|
|
27
|
+
# Check status
|
|
28
|
+
bilrost status
|
|
29
|
+
|
|
30
|
+
# SSH into the VM
|
|
31
|
+
bilrost ssh
|
|
32
|
+
|
|
33
|
+
# Sync overlay changes to host (with secret scanning)
|
|
34
|
+
bilrost sync
|
|
35
|
+
|
|
36
|
+
# Stop / destroy
|
|
37
|
+
bilrost down
|
|
38
|
+
bilrost destroy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## MCP Server
|
|
42
|
+
|
|
43
|
+
Agents can manage the sandbox programmatically via FastMCP:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"sandbox": {
|
|
49
|
+
"command": "bilrost-mcp"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
9 tools: `sandbox_status`, `sandbox_up`, `sandbox_down`, `sandbox_destroy`, `sandbox_exec`, `sandbox_validate`, `sandbox_ssh_info`, `sandbox_gateway_info`, `sandbox_agent_identity`.
|
|
56
|
+
|
|
57
|
+
## What It Does
|
|
58
|
+
|
|
59
|
+
- **OverlayFS isolation** — host code mounted read-only, all writes contained in VM overlay
|
|
60
|
+
- **Network containment** — UFW firewall with explicit allowlist (HTTPS, DNS, Tailscale, NTP only)
|
|
61
|
+
- **Secrets management** — three injection methods, `0600` perms, never in process lists
|
|
62
|
+
- **Gated sync** — gitleaks scanning + path allowlist before changes reach your host
|
|
63
|
+
- **Docker sandboxing** — per-session containers with configurable network isolation
|
|
64
|
+
- **12 Ansible roles** — overlay, secrets, gateway, docker, firewall, sync-gate, gh-cli, buildlog, cadence, qortex, tailscale, and more
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- macOS (Apple Silicon or Intel)
|
|
69
|
+
- [Homebrew](https://brew.sh/)
|
|
70
|
+
- ~10GB disk space
|
|
71
|
+
|
|
72
|
+
Dependencies (Lima, Ansible, etc.) are installed automatically on first run.
|
|
73
|
+
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
Full docs: [peleke.github.io/openclaw-sandbox](https://peleke.github.io/openclaw-sandbox/)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bilrost"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Hardened Lima VM for running AI agents — overlay isolation, network containment, secrets management, and gated sync."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Peleke Sengstacke" }]
|
|
13
|
+
keywords = ["sandbox", "ai-agents", "lima", "vm", "security", "isolation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
"Topic :: System :: Emulators",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"typer>=0.12,<1",
|
|
31
|
+
"pydantic>=2,<3",
|
|
32
|
+
"tomli>=2,<3;python_version<'3.11'",
|
|
33
|
+
"tomli-w>=1,<2",
|
|
34
|
+
"rich>=13,<14",
|
|
35
|
+
"jinja2>=3.1,<4",
|
|
36
|
+
"fastmcp>=2,<3",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["pytest>=8,<9", "pyyaml>=6,<7"]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/Peleke/openclaw-sandbox"
|
|
44
|
+
Documentation = "https://peleke.github.io/openclaw-sandbox/"
|
|
45
|
+
Repository = "https://github.com/Peleke/openclaw-sandbox"
|
|
46
|
+
Issues = "https://github.com/Peleke/openclaw-sandbox/issues"
|
|
47
|
+
Changelog = "https://github.com/Peleke/openclaw-sandbox/blob/main/CHANGELOG.md"
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
bilrost = "sandbox_cli.app:app"
|
|
51
|
+
bilrost-mcp = "sandbox_cli.mcp_server:main"
|
|
52
|
+
# Keep legacy entry points for backward compat
|
|
53
|
+
sandbox = "sandbox_cli.app:app"
|
|
54
|
+
sandbox-mcp = "sandbox_cli.mcp_server:main"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/sandbox_cli"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = [
|
|
61
|
+
"src/sandbox_cli/",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Output capture utilities for MCP stdio transport safety.
|
|
2
|
+
|
|
3
|
+
Rich console output and subprocess stdout must not leak into the MCP
|
|
4
|
+
stdio channel. This module provides helpers to capture or suppress
|
|
5
|
+
output so tool implementations stay transport-safe.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import io
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CapturedExec:
|
|
22
|
+
"""Result of a captured subprocess execution."""
|
|
23
|
+
|
|
24
|
+
stdout: str
|
|
25
|
+
stderr: str
|
|
26
|
+
exit_code: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_capture_console() -> Console:
|
|
30
|
+
"""Return a Rich console that writes to an in-memory buffer.
|
|
31
|
+
|
|
32
|
+
The caller can retrieve the output via ``console.file.getvalue()``.
|
|
33
|
+
"""
|
|
34
|
+
return Console(file=io.StringIO(), force_terminal=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@contextlib.contextmanager
|
|
38
|
+
def suppress_stdout():
|
|
39
|
+
"""Context manager that redirects ``sys.stdout`` to ``os.devnull``.
|
|
40
|
+
|
|
41
|
+
Useful when calling functions that may print directly (not via Rich)
|
|
42
|
+
or when ``subprocess.run`` inherits stdout.
|
|
43
|
+
"""
|
|
44
|
+
devnull = open(os.devnull, "w")
|
|
45
|
+
old_stdout = sys.stdout
|
|
46
|
+
try:
|
|
47
|
+
sys.stdout = devnull
|
|
48
|
+
yield
|
|
49
|
+
finally:
|
|
50
|
+
sys.stdout = old_stdout
|
|
51
|
+
devnull.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_captured(fn: Callable[..., object], *args: object, **kwargs: object) -> str:
|
|
55
|
+
"""Call *fn* with a capture console and return the output text.
|
|
56
|
+
|
|
57
|
+
The function must accept a ``console`` keyword argument (matching
|
|
58
|
+
the pattern used by ``print_post_bootstrap`` and ``print_status_report``).
|
|
59
|
+
"""
|
|
60
|
+
cap = make_capture_console()
|
|
61
|
+
fn(*args, console=cap, **kwargs)
|
|
62
|
+
return cap.file.getvalue()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _truncate(text: str, max_chars: int = 50_000) -> str:
|
|
66
|
+
"""Truncate *text* to *max_chars*, appending a marker if clipped."""
|
|
67
|
+
if len(text) <= max_chars:
|
|
68
|
+
return text
|
|
69
|
+
return text[:max_chars] + "\n\n[output truncated]"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Ansible inventory builder and playbook invocation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .lima_config import secrets_filename
|
|
12
|
+
from .lima_manager import SSHDetails
|
|
13
|
+
from .models import SandboxProfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_inventory(vm_name: str, ssh: SSHDetails) -> str:
|
|
17
|
+
"""Return an INI-format Ansible inventory string."""
|
|
18
|
+
return (
|
|
19
|
+
"[sandbox]\n"
|
|
20
|
+
f"{vm_name} "
|
|
21
|
+
f"ansible_host={ssh.host} "
|
|
22
|
+
f"ansible_port={ssh.port} "
|
|
23
|
+
f"ansible_user={ssh.user} "
|
|
24
|
+
f"ansible_ssh_private_key_file={ssh.key_path} "
|
|
25
|
+
"ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_extra_vars(profile: SandboxProfile) -> list[str]:
|
|
30
|
+
"""Build the ``-e key=value`` argument list for ``ansible-playbook``.
|
|
31
|
+
|
|
32
|
+
Matches the exact set of variables that ``bootstrap.sh`` passes.
|
|
33
|
+
"""
|
|
34
|
+
sec_fname = secrets_filename(profile)
|
|
35
|
+
tenant = getpass.getuser()
|
|
36
|
+
|
|
37
|
+
# Conditional mount paths — empty string when not configured
|
|
38
|
+
agent_mount = "/mnt/openclaw-agents" if profile.mounts.agent_data else ""
|
|
39
|
+
buildlog_mount = "/mnt/buildlog-data" if profile.mounts.buildlog_data else ""
|
|
40
|
+
|
|
41
|
+
pairs: list[tuple[str, str]] = [
|
|
42
|
+
("tenant_name", tenant),
|
|
43
|
+
("provision_path", "/mnt/provision"),
|
|
44
|
+
("openclaw_path", "/mnt/openclaw"),
|
|
45
|
+
("obsidian_path", "/mnt/obsidian"),
|
|
46
|
+
("secrets_filename", sec_fname),
|
|
47
|
+
("overlay_yolo_mode", str(profile.mode.yolo).lower()),
|
|
48
|
+
("overlay_yolo_unsafe", str(profile.mode.yolo_unsafe).lower()),
|
|
49
|
+
("docker_enabled", str(not profile.mode.no_docker).lower()),
|
|
50
|
+
("agent_data_mount", agent_mount),
|
|
51
|
+
("buildlog_data_mount", buildlog_mount),
|
|
52
|
+
("memgraph_enabled", str(profile.mode.memgraph).lower()),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
argv: list[str] = []
|
|
56
|
+
for key, value in pairs:
|
|
57
|
+
argv.extend(["-e", f"{key}={value}"])
|
|
58
|
+
|
|
59
|
+
# User-supplied extra vars
|
|
60
|
+
for key, value in profile.extra_vars.items():
|
|
61
|
+
argv.extend(["-e", f"{key}={value}"])
|
|
62
|
+
|
|
63
|
+
return argv
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_playbook(
|
|
67
|
+
profile: SandboxProfile,
|
|
68
|
+
ssh: SSHDetails,
|
|
69
|
+
bootstrap_dir: Path,
|
|
70
|
+
*,
|
|
71
|
+
vm_name: str = "openclaw-sandbox",
|
|
72
|
+
) -> int:
|
|
73
|
+
"""Write a temp inventory, run ``ansible-playbook``, clean up.
|
|
74
|
+
|
|
75
|
+
Returns the ansible-playbook exit code.
|
|
76
|
+
"""
|
|
77
|
+
inventory_text = build_inventory(vm_name, ssh)
|
|
78
|
+
playbook = bootstrap_dir / "ansible" / "playbook.yml"
|
|
79
|
+
|
|
80
|
+
fd, inv_path = tempfile.mkstemp(prefix="sandbox-inv-", suffix=".ini")
|
|
81
|
+
try:
|
|
82
|
+
with os.fdopen(fd, "w") as f:
|
|
83
|
+
f.write(inventory_text)
|
|
84
|
+
|
|
85
|
+
cmd = [
|
|
86
|
+
"ansible-playbook",
|
|
87
|
+
"-i",
|
|
88
|
+
inv_path,
|
|
89
|
+
str(playbook),
|
|
90
|
+
] + build_extra_vars(profile)
|
|
91
|
+
|
|
92
|
+
env = {**os.environ, "ANSIBLE_HOST_KEY_CHECKING": "False"}
|
|
93
|
+
result = subprocess.run(cmd, env=env)
|
|
94
|
+
return result.returncode
|
|
95
|
+
finally:
|
|
96
|
+
Path(inv_path).unlink(missing_ok=True)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""OpenClaw Sandbox CLI — Typer app and subcommand definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .bootstrap import find_bootstrap_dir, run_script
|
|
11
|
+
from .dashboard import run_dashboard_sync
|
|
12
|
+
from .lima_manager import LimaManager
|
|
13
|
+
from .models import SandboxProfile
|
|
14
|
+
from .orchestrator import orchestrate_up
|
|
15
|
+
from .profile import init_wizard, load_profile
|
|
16
|
+
from .reporting import print_status_report
|
|
17
|
+
from .validation import validate_profile
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(
|
|
20
|
+
name="sandbox",
|
|
21
|
+
help="OpenClaw Sandbox — provision once, run forever.",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
_ONBOARD_CMD = (
|
|
27
|
+
'cd "$(if mountpoint -q /workspace 2>/dev/null; '
|
|
28
|
+
"then echo /workspace; else echo /mnt/openclaw; fi)\" "
|
|
29
|
+
"&& node dist/index.js onboard"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── helpers ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_and_validate(*, strict: bool = True) -> SandboxProfile:
|
|
37
|
+
"""Load the profile and run validation. Exit on errors if strict."""
|
|
38
|
+
profile = load_profile()
|
|
39
|
+
result = validate_profile(profile)
|
|
40
|
+
for w in result.warnings:
|
|
41
|
+
console.print(f"[yellow]warning:[/yellow] {w}")
|
|
42
|
+
if not result.ok:
|
|
43
|
+
for e in result.errors:
|
|
44
|
+
console.print(f"[red]error:[/red] {e}")
|
|
45
|
+
if strict:
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
return profile
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── subcommands ──────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def init() -> None:
|
|
55
|
+
"""Interactive wizard to create or update your sandbox profile."""
|
|
56
|
+
init_wizard()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
def up(
|
|
61
|
+
fresh: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
typer.Option("--fresh", help="Destroy existing VM first, then reprovision."),
|
|
64
|
+
] = False,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Provision (or reprovision) the sandbox VM."""
|
|
67
|
+
profile = _load_and_validate()
|
|
68
|
+
bootstrap_dir = find_bootstrap_dir(profile)
|
|
69
|
+
lima = LimaManager()
|
|
70
|
+
if fresh:
|
|
71
|
+
console.print("[bold]Destroying existing VM before reprovisioning...[/bold]")
|
|
72
|
+
lima.delete()
|
|
73
|
+
rc = orchestrate_up(profile, bootstrap_dir, lima=lima)
|
|
74
|
+
raise typer.Exit(rc)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def down() -> None:
|
|
79
|
+
"""Stop the sandbox VM (force kill)."""
|
|
80
|
+
_load_and_validate(strict=False)
|
|
81
|
+
lima = LimaManager()
|
|
82
|
+
lima.stop(force=True)
|
|
83
|
+
console.print("VM stopped.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def destroy(
|
|
88
|
+
force: Annotated[
|
|
89
|
+
bool,
|
|
90
|
+
typer.Option("-f", "--force", help="Skip confirmation prompt."),
|
|
91
|
+
] = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Delete the sandbox VM entirely."""
|
|
94
|
+
if not force:
|
|
95
|
+
confirm = typer.confirm("This will permanently delete the VM. Continue?")
|
|
96
|
+
if not confirm:
|
|
97
|
+
raise typer.Abort()
|
|
98
|
+
_load_and_validate(strict=False)
|
|
99
|
+
lima = LimaManager()
|
|
100
|
+
lima.delete()
|
|
101
|
+
console.print("VM deleted.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command()
|
|
105
|
+
def status() -> None:
|
|
106
|
+
"""Show VM state and profile summary."""
|
|
107
|
+
profile = _load_and_validate(strict=False)
|
|
108
|
+
print_status_report(profile, console)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command()
|
|
112
|
+
def ssh() -> None:
|
|
113
|
+
"""SSH into the sandbox VM (replaces process for TTY)."""
|
|
114
|
+
_load_and_validate(strict=False)
|
|
115
|
+
lima = LimaManager()
|
|
116
|
+
lima.shell()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def onboard() -> None:
|
|
121
|
+
"""Run the onboarding wizard inside the VM (replaces process for TTY)."""
|
|
122
|
+
_load_and_validate(strict=False)
|
|
123
|
+
lima = LimaManager()
|
|
124
|
+
lima.shell_exec(_ONBOARD_CMD)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def sync(
|
|
129
|
+
dry_run: Annotated[
|
|
130
|
+
bool,
|
|
131
|
+
typer.Option("--dry-run", help="Preview changes without applying."),
|
|
132
|
+
] = False,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Sync overlay changes from VM to host."""
|
|
135
|
+
profile = _load_and_validate(strict=False)
|
|
136
|
+
flags = []
|
|
137
|
+
if dry_run:
|
|
138
|
+
flags.append("--dry-run")
|
|
139
|
+
rc = run_script(profile, "sync-gate.sh", extra_flags=flags)
|
|
140
|
+
raise typer.Exit(rc)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── dashboard sub-app ────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
dashboard_app = typer.Typer(
|
|
146
|
+
name="dashboard",
|
|
147
|
+
help="Gateway dashboard and GitHub-to-Obsidian sync.",
|
|
148
|
+
invoke_without_command=True,
|
|
149
|
+
)
|
|
150
|
+
app.add_typer(dashboard_app)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dashboard_app.callback(invoke_without_command=True)
|
|
154
|
+
def dashboard_open(
|
|
155
|
+
ctx: typer.Context,
|
|
156
|
+
page: Annotated[
|
|
157
|
+
Optional[str],
|
|
158
|
+
typer.Option("--page", "-p", help="Dashboard page: control, green, learning"),
|
|
159
|
+
] = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Open the OpenClaw gateway dashboard."""
|
|
162
|
+
if ctx.invoked_subcommand is not None:
|
|
163
|
+
return
|
|
164
|
+
profile = _load_and_validate(strict=False)
|
|
165
|
+
flags = []
|
|
166
|
+
if page:
|
|
167
|
+
flags.append(page)
|
|
168
|
+
rc = run_script(profile, "dashboard.sh", extra_flags=flags)
|
|
169
|
+
raise typer.Exit(rc)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dashboard_app.command("sync")
|
|
173
|
+
def dashboard_sync(
|
|
174
|
+
dry_run: Annotated[
|
|
175
|
+
bool,
|
|
176
|
+
typer.Option("--dry-run", help="Preview without writing files."),
|
|
177
|
+
] = False,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Sync GitHub issues to Obsidian kanban boards."""
|
|
180
|
+
profile = _load_and_validate(strict=False)
|
|
181
|
+
try:
|
|
182
|
+
result = run_dashboard_sync(profile, dry_run=dry_run)
|
|
183
|
+
except FileNotFoundError as exc:
|
|
184
|
+
console.print(f"[red]error:[/red] {exc}")
|
|
185
|
+
raise typer.Exit(1) from None
|
|
186
|
+
|
|
187
|
+
if result.stdout:
|
|
188
|
+
console.print(result.stdout.rstrip())
|
|
189
|
+
if result.returncode != 0:
|
|
190
|
+
if result.stderr:
|
|
191
|
+
console.print(f"[yellow]{result.stderr.rstrip()}[/yellow]")
|
|
192
|
+
console.print(f"[red]Sync failed (exit {result.returncode}).[/red]")
|
|
193
|
+
raise typer.Exit(result.returncode)
|
|
194
|
+
console.print("[green]Dashboard sync complete.[/green]")
|