hyperbox-mcp 0.2.1__py3-none-any.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.
- hyperbox_mcp/__init__.py +0 -0
- hyperbox_mcp/builder.py +90 -0
- hyperbox_mcp/cli.py +164 -0
- hyperbox_mcp/clientconfig.py +203 -0
- hyperbox_mcp/doctor.py +427 -0
- hyperbox_mcp/engine.py +727 -0
- hyperbox_mcp/filelock.py +114 -0
- hyperbox_mcp/llm_sandbox_runtime.py +750 -0
- hyperbox_mcp/policy.py +171 -0
- hyperbox_mcp/registry.py +328 -0
- hyperbox_mcp/runtime.py +136 -0
- hyperbox_mcp/server.py +701 -0
- hyperbox_mcp/validate.py +224 -0
- hyperbox_mcp-0.2.1.dist-info/METADATA +227 -0
- hyperbox_mcp-0.2.1.dist-info/RECORD +19 -0
- hyperbox_mcp-0.2.1.dist-info/WHEEL +5 -0
- hyperbox_mcp-0.2.1.dist-info/entry_points.txt +2 -0
- hyperbox_mcp-0.2.1.dist-info/licenses/LICENSE +21 -0
- hyperbox_mcp-0.2.1.dist-info/top_level.txt +1 -0
hyperbox_mcp/__init__.py
ADDED
|
File without changes
|
hyperbox_mcp/builder.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Building custom sandbox environments, from a terminal only.
|
|
2
|
+
|
|
3
|
+
This is deliberately not reachable from the MCP tool surface. A build
|
|
4
|
+
runs whatever the Dockerfile says — arbitrary commands, as root, with
|
|
5
|
+
network access, under none of the limits policy.py enforces on a
|
|
6
|
+
sandbox. That is a decision for the person at the keyboard, not for a
|
|
7
|
+
model. Agents consume the result by name; they never produce it.
|
|
8
|
+
|
|
9
|
+
Anything here may print to stdout: it runs under the `hyperbox` CLI,
|
|
10
|
+
never inside the server process, whose stdout belongs to the client.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from hyperbox_mcp import engine, policy, validate
|
|
20
|
+
|
|
21
|
+
HYPERBOX_DIR = Path.home() / ".hyperbox"
|
|
22
|
+
ENV_DIR = HYPERBOX_DIR / "environments"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_environments() -> int:
|
|
26
|
+
"""Print every environment the server would currently resolve."""
|
|
27
|
+
envs = policy.environments()
|
|
28
|
+
print(f"{'ENVIRONMENT':<20} {'IMAGE':<48} {'DOCKERFILE'}")
|
|
29
|
+
print("-" * 82)
|
|
30
|
+
for name, image in sorted(envs.items()):
|
|
31
|
+
dockerfile = ENV_DIR / name / "Dockerfile"
|
|
32
|
+
print(f"{name:<20} {image:<48} {'yes' if dockerfile.exists() else 'built-in'}")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_build(env_name: str, custom_path: str | None = None) -> int:
|
|
37
|
+
"""Build a local Dockerfile into the image `create_sandbox` will use."""
|
|
38
|
+
# Validated here too, not only at the MCP boundary: env_name becomes a
|
|
39
|
+
# directory under ~/.hyperbox and an image tag, and this path never
|
|
40
|
+
# goes through the server.
|
|
41
|
+
try:
|
|
42
|
+
name = validate.environment_name(env_name)
|
|
43
|
+
except validate.InvalidInput as exc:
|
|
44
|
+
print(f"Error: {exc}")
|
|
45
|
+
return 2
|
|
46
|
+
|
|
47
|
+
env_dir = ENV_DIR / name
|
|
48
|
+
dockerfile = env_dir / "Dockerfile"
|
|
49
|
+
|
|
50
|
+
if custom_path:
|
|
51
|
+
source = Path(custom_path).expanduser().resolve()
|
|
52
|
+
if not source.is_file():
|
|
53
|
+
print(f"Error: no Dockerfile at {source}")
|
|
54
|
+
return 1
|
|
55
|
+
env_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
shutil.copy2(source, dockerfile)
|
|
57
|
+
print(f"Copied {source} -> {dockerfile}")
|
|
58
|
+
|
|
59
|
+
if not dockerfile.exists():
|
|
60
|
+
print(f"Error: no Dockerfile at {dockerfile}")
|
|
61
|
+
print("Provide one with:")
|
|
62
|
+
print(f" hyperbox build {name} --custom /path/to/Dockerfile")
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
backend = engine.detect("auto")
|
|
67
|
+
except Exception as exc: # noqa: BLE001 - reported, not swallowed
|
|
68
|
+
print(f"Error: no container engine available: {exc}")
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
target_image = f"hyperbox-local/{name}:latest"
|
|
72
|
+
binary = engine.client_dialect(backend)
|
|
73
|
+
cmd = [binary, "build", "-t", target_image, "-f", str(dockerfile), str(env_dir)]
|
|
74
|
+
|
|
75
|
+
print(f"Building '{name}' as {target_image} using {binary}...")
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(cmd, check=False)
|
|
78
|
+
except KeyboardInterrupt:
|
|
79
|
+
print("\nBuild cancelled.")
|
|
80
|
+
return 130
|
|
81
|
+
except OSError as exc:
|
|
82
|
+
print(f"\nCould not run {binary}: {exc}")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
if result.returncode == 0:
|
|
86
|
+
print(f"\nBuilt '{name}'. Use it with create_sandbox(environment='{name}').")
|
|
87
|
+
print("A running server picks it up without a restart.")
|
|
88
|
+
else:
|
|
89
|
+
print(f"\nBuild failed with exit code {result.returncode}.")
|
|
90
|
+
return result.returncode
|
hyperbox_mcp/cli.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Terminal subcommands for the `hyperbox` executable.
|
|
2
|
+
|
|
3
|
+
Kept apart from server.py so that starting the MCP server imports none of
|
|
4
|
+
it. Anything here may print to stdout; the server may not, because a
|
|
5
|
+
stdio MCP client is reading that stream.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def usage() -> str:
|
|
15
|
+
return (
|
|
16
|
+
"Usage:\n"
|
|
17
|
+
" hyperbox Start the MCP server on stdio (default)\n"
|
|
18
|
+
" hyperbox doctor Check this machine can run sandboxes\n"
|
|
19
|
+
" --pull Also pull the sandbox image if missing\n"
|
|
20
|
+
" --quick Skip the live create/run/destroy check\n"
|
|
21
|
+
" hyperbox config Print a ready-to-paste MCP client config\n"
|
|
22
|
+
" --format json mcpServers block (Claude Desktop, generic)\n"
|
|
23
|
+
" --format cursor servers block (.vscode/mcp.json)\n"
|
|
24
|
+
" --format yaml YAML list (Continue-based clients)\n"
|
|
25
|
+
" --format antigravity Antigravity mcp_config.json\n"
|
|
26
|
+
" hyperbox envs List environments create_sandbox can use\n"
|
|
27
|
+
" hyperbox build <name> Build an environment from a Dockerfile\n"
|
|
28
|
+
" --custom <path> Copy that Dockerfile in and build it\n"
|
|
29
|
+
" hyperbox logs Show the server log\n"
|
|
30
|
+
" --follow Keep printing as new lines arrive\n"
|
|
31
|
+
" hyperbox --version Print the installed version\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _version() -> str:
|
|
36
|
+
try:
|
|
37
|
+
return version("hyperbox-mcp")
|
|
38
|
+
except PackageNotFoundError: # pragma: no cover - running from source
|
|
39
|
+
return "unknown (not installed as a package)"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _logs(follow: bool = False) -> int:
|
|
43
|
+
"""Print the server log, optionally following it.
|
|
44
|
+
|
|
45
|
+
Written in Python rather than shelling out to `tail -f`: this package
|
|
46
|
+
is expected to work on Windows, where there is no tail, and building
|
|
47
|
+
a shell command out of a home-directory path invites quoting bugs.
|
|
48
|
+
"""
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
from hyperbox_mcp.server import LOG_FILE
|
|
52
|
+
|
|
53
|
+
if not LOG_FILE.exists():
|
|
54
|
+
print(f"No log yet at {LOG_FILE}")
|
|
55
|
+
print("It is created when the server next starts.")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
with LOG_FILE.open("r", encoding="utf-8", errors="replace") as fh:
|
|
59
|
+
sys.stdout.write(fh.read())
|
|
60
|
+
if not follow:
|
|
61
|
+
return 0
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
try:
|
|
64
|
+
while True:
|
|
65
|
+
line = fh.readline()
|
|
66
|
+
if line:
|
|
67
|
+
sys.stdout.write(line)
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
else:
|
|
70
|
+
time.sleep(0.4)
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def dispatch(argv: list[str]) -> int:
|
|
76
|
+
command, *rest = argv
|
|
77
|
+
|
|
78
|
+
if command in {"--version", "-V"}:
|
|
79
|
+
print(f"hyperbox {_version()}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if command in {"help", "--help", "-h"}:
|
|
83
|
+
print(usage())
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
if command == "doctor":
|
|
87
|
+
unknown = [a for a in rest if a not in {"--pull", "--quick"}]
|
|
88
|
+
if unknown:
|
|
89
|
+
print(f"hyperbox doctor: unknown option {unknown[0]!r}\n")
|
|
90
|
+
print(usage())
|
|
91
|
+
return 2
|
|
92
|
+
from hyperbox_mcp.doctor import run_doctor
|
|
93
|
+
|
|
94
|
+
return run_doctor(pull="--pull" in rest, live="--quick" not in rest)
|
|
95
|
+
|
|
96
|
+
if command == "config":
|
|
97
|
+
fmt = "json"
|
|
98
|
+
rest_iter = list(rest)
|
|
99
|
+
while rest_iter:
|
|
100
|
+
arg = rest_iter.pop(0)
|
|
101
|
+
if arg == "--format":
|
|
102
|
+
if not rest_iter:
|
|
103
|
+
print("hyperbox config: --format needs a value\n")
|
|
104
|
+
print(usage())
|
|
105
|
+
return 2
|
|
106
|
+
fmt = rest_iter.pop(0)
|
|
107
|
+
elif arg.startswith("--format="):
|
|
108
|
+
fmt = arg.split("=", 1)[1]
|
|
109
|
+
else:
|
|
110
|
+
print(f"hyperbox config: unknown option {arg!r}\n")
|
|
111
|
+
print(usage())
|
|
112
|
+
return 2
|
|
113
|
+
if fmt not in {"json", "cursor", "yaml", "antigravity"}:
|
|
114
|
+
print(f"hyperbox config: unknown format {fmt!r}. "
|
|
115
|
+
"Use json, cursor, yaml or antigravity.\n")
|
|
116
|
+
return 2
|
|
117
|
+
from hyperbox_mcp.clientconfig import print_config
|
|
118
|
+
|
|
119
|
+
return print_config(fmt)
|
|
120
|
+
|
|
121
|
+
if command == "logs":
|
|
122
|
+
unknown = [a for a in rest if a not in {"--follow", "-f"}]
|
|
123
|
+
if unknown:
|
|
124
|
+
print(f"hyperbox logs: unknown option {unknown[0]!r}\n")
|
|
125
|
+
print(usage())
|
|
126
|
+
return 2
|
|
127
|
+
return _logs(follow=bool(rest))
|
|
128
|
+
|
|
129
|
+
if command == "envs":
|
|
130
|
+
if rest:
|
|
131
|
+
print(f"hyperbox envs: unexpected argument {rest[0]!r}\n")
|
|
132
|
+
print(usage())
|
|
133
|
+
return 2
|
|
134
|
+
from hyperbox_mcp.builder import list_environments
|
|
135
|
+
|
|
136
|
+
return list_environments()
|
|
137
|
+
|
|
138
|
+
if command == "build":
|
|
139
|
+
if not rest:
|
|
140
|
+
print("hyperbox build: missing environment name\n")
|
|
141
|
+
print(usage())
|
|
142
|
+
return 2
|
|
143
|
+
name, *opts = rest
|
|
144
|
+
custom = None
|
|
145
|
+
while opts:
|
|
146
|
+
arg = opts.pop(0)
|
|
147
|
+
if arg == "--custom":
|
|
148
|
+
if not opts:
|
|
149
|
+
print("hyperbox build: --custom needs a path\n")
|
|
150
|
+
print(usage())
|
|
151
|
+
return 2
|
|
152
|
+
custom = opts.pop(0)
|
|
153
|
+
elif arg.startswith("--custom="):
|
|
154
|
+
custom = arg.split("=", 1)[1]
|
|
155
|
+
else:
|
|
156
|
+
print(f"hyperbox build: unknown option {arg!r}\n")
|
|
157
|
+
print(usage())
|
|
158
|
+
return 2
|
|
159
|
+
from hyperbox_mcp.builder import run_build
|
|
160
|
+
|
|
161
|
+
return run_build(name, custom)
|
|
162
|
+
|
|
163
|
+
print(usage())
|
|
164
|
+
return 2
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Generate a ready-to-paste MCP client configuration.
|
|
2
|
+
|
|
3
|
+
Every value in a client config is something this process already knows:
|
|
4
|
+
where its own executable lives, and where the container CLI lives. Asking
|
|
5
|
+
a person to retype those by hand is where setup actually fails — most
|
|
6
|
+
sharply on Windows, where an absolute path routinely contains a space and
|
|
7
|
+
every backslash has to be escaped for JSON, in a file whose only failure
|
|
8
|
+
mode is "no tools appeared" with no error anywhere.
|
|
9
|
+
|
|
10
|
+
So the escaping is done by serialising rather than by hand. `json.dumps`
|
|
11
|
+
gets Windows paths right by construction, and YAML's double-quoted
|
|
12
|
+
scalars use the same escape rules, so the YAML output quotes its strings
|
|
13
|
+
the same way — which is also why this needs no YAML dependency.
|
|
14
|
+
|
|
15
|
+
Notes go to stderr and the config to stdout, so `hyperbox config >
|
|
16
|
+
mcp.json` writes a clean file while a person still sees the guidance.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from hyperbox_mcp import engine, policy
|
|
28
|
+
|
|
29
|
+
#: The server name a client will show these tools under.
|
|
30
|
+
SERVER_NAME = "hyperbox"
|
|
31
|
+
|
|
32
|
+
#: Directories that must be on PATH for the server to find its engine,
|
|
33
|
+
#: beyond whatever the engine CLI's own directory turns out to be.
|
|
34
|
+
_SYSTEM_PATH_DIRS = {
|
|
35
|
+
"win32": (r"C:\Windows\System32", r"C:\Windows"),
|
|
36
|
+
}
|
|
37
|
+
_POSIX_PATH_DIRS = ("/usr/local/bin", "/usr/bin", "/bin")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def executable_path() -> str:
|
|
41
|
+
"""Absolute path to the `hyperbox` command a client should launch.
|
|
42
|
+
|
|
43
|
+
`shutil.which` is preferred over `sys.argv[0]`: it resolves the name
|
|
44
|
+
the way a client would, which is the thing being configured.
|
|
45
|
+
"""
|
|
46
|
+
found = shutil.which("hyperbox")
|
|
47
|
+
if found:
|
|
48
|
+
return os.path.abspath(found)
|
|
49
|
+
# Running as `python -m hyperbox_mcp.server`, or a console script not
|
|
50
|
+
# on PATH. argv[0] still names something launchable.
|
|
51
|
+
argv0 = sys.argv[0] or ""
|
|
52
|
+
if argv0 and os.path.exists(argv0):
|
|
53
|
+
return os.path.abspath(argv0)
|
|
54
|
+
return "hyperbox"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _looks_like_a_checkout(executable: str) -> bool:
|
|
58
|
+
"""Whether this executable lives inside a source tree rather than an
|
|
59
|
+
installed tool — `uv run` in a clone produces exactly that."""
|
|
60
|
+
parts = {p.lower() for p in Path(executable).parts}
|
|
61
|
+
return bool({".venv", "venv"} & parts)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def path_entries() -> list[str]:
|
|
65
|
+
"""Directories a client must put on PATH, most specific first.
|
|
66
|
+
|
|
67
|
+
Clients launch servers with a trimmed environment, so the container
|
|
68
|
+
CLI is frequently unreachable unless it is named explicitly. The
|
|
69
|
+
engine probe already knows where it is.
|
|
70
|
+
"""
|
|
71
|
+
entries: list[str] = []
|
|
72
|
+
|
|
73
|
+
def add(directory: str) -> None:
|
|
74
|
+
if directory and directory not in entries:
|
|
75
|
+
entries.append(directory)
|
|
76
|
+
|
|
77
|
+
# The executable's own directory, so a client that resolves by name
|
|
78
|
+
# still finds it.
|
|
79
|
+
add(os.path.dirname(executable_path()))
|
|
80
|
+
|
|
81
|
+
for backend in policy.BACKENDS:
|
|
82
|
+
try:
|
|
83
|
+
status = engine.probe(backend)
|
|
84
|
+
except Exception: # noqa: BLE001 - a config is useful without an engine
|
|
85
|
+
continue
|
|
86
|
+
if status.binary:
|
|
87
|
+
add(os.path.dirname(status.binary))
|
|
88
|
+
|
|
89
|
+
for directory in _SYSTEM_PATH_DIRS.get(sys.platform, _POSIX_PATH_DIRS):
|
|
90
|
+
add(directory)
|
|
91
|
+
return entries
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _server_entry() -> dict:
|
|
95
|
+
separator = ";" if sys.platform == "win32" else ":"
|
|
96
|
+
return {
|
|
97
|
+
"command": executable_path(),
|
|
98
|
+
"args": [],
|
|
99
|
+
"env": {"PATH": separator.join(path_entries())},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _yaml_scalar(value: str) -> str:
|
|
104
|
+
"""A double-quoted YAML scalar.
|
|
105
|
+
|
|
106
|
+
YAML's double-quoted style uses JSON's escape rules, so serialising
|
|
107
|
+
with json.dumps produces a correct — and correctly escaped — scalar.
|
|
108
|
+
That is the whole point: Windows backslashes are handled by the
|
|
109
|
+
serialiser rather than by whoever is editing the file.
|
|
110
|
+
"""
|
|
111
|
+
return json.dumps(value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def render(fmt: str = "json") -> str:
|
|
115
|
+
"""The configuration block for `fmt`, ready to paste."""
|
|
116
|
+
entry = _server_entry()
|
|
117
|
+
|
|
118
|
+
if fmt == "yaml":
|
|
119
|
+
# Continue-based clients: a YAML list under mcpServers.
|
|
120
|
+
lines = [
|
|
121
|
+
"mcpServers:",
|
|
122
|
+
f" - name: {SERVER_NAME}",
|
|
123
|
+
f" command: {_yaml_scalar(entry['command'])}",
|
|
124
|
+
" args: []",
|
|
125
|
+
" env:",
|
|
126
|
+
f" PATH: {_yaml_scalar(entry['env']['PATH'])}",
|
|
127
|
+
]
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
# Cursor and VS Code use "servers" in .vscode/mcp.json; Claude Desktop,
|
|
131
|
+
# Antigravity and most others use "mcpServers". Same object either way,
|
|
132
|
+
# so antigravity needs no branch here — only a different file to put it
|
|
133
|
+
# in, which notes() names.
|
|
134
|
+
key = "servers" if fmt == "cursor" else "mcpServers"
|
|
135
|
+
return json.dumps({key: {SERVER_NAME: entry}}, indent=2)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def notes(fmt: str) -> list[str]:
|
|
139
|
+
"""Guidance printed to stderr, so stdout stays paste-clean."""
|
|
140
|
+
executable = executable_path()
|
|
141
|
+
out = []
|
|
142
|
+
|
|
143
|
+
if fmt == "yaml":
|
|
144
|
+
out.append(
|
|
145
|
+
"Continue-based clients: merge into your mcpservers YAML "
|
|
146
|
+
"config (often mcpservers/config.yaml)"
|
|
147
|
+
)
|
|
148
|
+
elif fmt == "cursor":
|
|
149
|
+
out.append("Cursor / VS Code: merge into .vscode/mcp.json")
|
|
150
|
+
elif fmt == "antigravity":
|
|
151
|
+
out.append(
|
|
152
|
+
"Antigravity: merge into ~/.gemini/antigravity/mcp_config.json"
|
|
153
|
+
)
|
|
154
|
+
out.append(
|
|
155
|
+
" (restart Antigravity afterwards; it reads the file at startup)"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
out.append("Merge into your client's MCP config (Claude Desktop: "
|
|
159
|
+
"claude_desktop_config.json)")
|
|
160
|
+
|
|
161
|
+
out.append("")
|
|
162
|
+
if _looks_like_a_checkout(executable):
|
|
163
|
+
out.append(
|
|
164
|
+
"WARNING: this command lives inside a source checkout, so the "
|
|
165
|
+
"config below is tied to that folder and breaks if you move or "
|
|
166
|
+
"delete it. Install it as a tool, then run this again:"
|
|
167
|
+
)
|
|
168
|
+
out.append(" uv tool install hyperbox-mcp")
|
|
169
|
+
out.append(f" (current: {executable})")
|
|
170
|
+
else:
|
|
171
|
+
out.append(
|
|
172
|
+
"No checkout is needed: this launches the installed executable "
|
|
173
|
+
"directly, so the repository can be moved or deleted."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
reachable = [b for b in policy.BACKENDS if engine.probe(b).reachable]
|
|
177
|
+
if reachable:
|
|
178
|
+
out.append("")
|
|
179
|
+
out.append(f"Container engine found: {', '.join(reachable)}")
|
|
180
|
+
else:
|
|
181
|
+
out.append("")
|
|
182
|
+
out.append(
|
|
183
|
+
"WARNING: no container engine is reachable right now. The "
|
|
184
|
+
"config below is still correct — start Docker or Podman, then "
|
|
185
|
+
"run `hyperbox doctor`."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
out.append("")
|
|
189
|
+
out.append(
|
|
190
|
+
"Before the first sandbox, pull the image once so a multi-gigabyte "
|
|
191
|
+
"download never happens inside a client request (clients time a "
|
|
192
|
+
"request out long before it could finish):"
|
|
193
|
+
)
|
|
194
|
+
out.append(" hyperbox doctor --pull")
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def print_config(fmt: str = "json") -> int:
|
|
199
|
+
for line in notes(fmt):
|
|
200
|
+
print(line, file=sys.stderr)
|
|
201
|
+
print("", file=sys.stderr)
|
|
202
|
+
print(render(fmt))
|
|
203
|
+
return 0
|