The Docker layer is responsible for producing buildable and runnable artifacts for OpenClaw environments.
openenv.docker¶
openenv.docker
¶
Docker image and compose generation helpers.
openenv.docker.dockerfile¶
openenv.docker.dockerfile
¶
Dockerfile generation for OpenClawenv.
render_dockerfile(manifest, lockfile, *, raw_manifest_text, raw_lock_text)
¶
Render a standalone Dockerfile from a manifest and lockfile.
Source code in src/openenv/docker/dockerfile.py
def render_dockerfile(
manifest: Manifest,
lockfile: Lockfile,
*,
raw_manifest_text: str,
raw_lock_text: str,
) -> str:
"""Render a standalone Dockerfile from a manifest and lockfile."""
sandbox_reference = lockfile.base_image["resolved_reference"]
payload = _render_payload(
manifest,
raw_manifest_text=raw_manifest_text,
raw_lock_text=raw_lock_text,
image_reference="${OPENCLAW_IMAGE}",
)
payload_b64 = encode_payload(payload)
lines: list[str] = [
"# syntax=docker/dockerfile:1",
f"FROM {OPENCLAW_GATEWAY_RUNTIME_IMAGE}",
f'LABEL org.opencontainers.image.title="{_escape_label(manifest.project.name)}"',
f'LABEL org.opencontainers.image.version="{_escape_label(manifest.project.version)}"',
f'LABEL io.openclawenv.manifest-hash="{lockfile.manifest_hash}"',
f'LABEL io.openclawenv.sandbox-image="{_escape_label(sandbox_reference)}"',
"ENV PYTHONDONTWRITEBYTECODE=1",
"USER root",
]
for key, value in sorted(manifest.runtime.env.items()):
lines.append(f"ENV {key}={json.dumps(value)}")
lines.extend(_package_install_lines(manifest.runtime.system_packages))
lines.extend(_python_binary_link_lines())
lines.extend(_python_venv_lines())
lines.append(
"RUN if ! command -v npx >/dev/null 2>&1; then "
"printf '%s\\n' '#!/bin/sh' 'exec npm exec --yes -- \"$@\"' "
"> /usr/local/bin/npx && chmod +x /usr/local/bin/npx; fi"
)
requirements = [
SKILL_SCANNER_REQUIREMENT,
*(_python_package_argument(package) for package in lockfile.python_packages),
]
lines.append(
"RUN python -m pip install --no-cache-dir " + " ".join(requirements)
)
node_requirements = _global_node_requirements(lockfile)
if node_requirements:
lines.append(
"RUN npm install --global --no-fund --no-update-notifier "
+ " ".join(node_requirements)
)
lines.append("RUN agent-browser install")
lines.extend(_optional_browser_install_lines())
lines.extend(_optional_docker_cli_install_lines())
lines.extend(_state_link_lines(manifest))
lines.append("RUN mkdir -p /opt/openclawenv")
lines.append(
'RUN ["python", "-c", '
f'{json.dumps(_payload_writer_script(payload_b64))}'
"]"
)
lines.extend(_catalog_skill_install_lines(manifest))
lines.extend(_skill_scan_lines(manifest))
lines.extend(_runtime_permission_lines(manifest))
lines.append(f"USER {_effective_runtime_user(manifest)}")
return "\n".join(lines) + "\n"
render_runtime_payload(manifest, lockfile, *, raw_manifest_text, raw_lock_text)
¶
Return the file payload embedded into the runtime image.
Source code in src/openenv/docker/dockerfile.py
def render_runtime_payload(
manifest: Manifest,
lockfile: Lockfile,
*,
raw_manifest_text: str,
raw_lock_text: str,
) -> dict[str, object]:
"""Return the file payload embedded into the runtime image."""
return _render_payload(
manifest,
raw_manifest_text=raw_manifest_text,
raw_lock_text=raw_lock_text,
image_reference="${OPENCLAW_IMAGE}",
)
openenv.docker.compose¶
openenv.docker.compose
¶
docker-compose generation for OpenClawenv.
AllBotsComposeSpec
dataclass
¶
Information needed to render one bot entry inside the shared compose stack.
Source code in src/openenv/docker/compose.py
@dataclass(slots=True, frozen=True)
class AllBotsComposeSpec:
"""Information needed to render one bot entry inside the shared compose stack."""
slug: str
manifest: Manifest
image_tag: str
all_bots_compose_filename()
¶
Return the shared compose filename for all managed bots.
Source code in src/openenv/docker/compose.py
def all_bots_compose_filename() -> str:
"""Return the shared compose filename for all managed bots."""
return ALL_BOTS_COMPOSE_FILENAME
all_bots_env_filename()
¶
Return the shared env filename used by the all-bots stack.
Source code in src/openenv/docker/compose.py
def all_bots_env_filename() -> str:
"""Return the shared env filename used by the all-bots stack."""
return ALL_BOTS_ENV_FILENAME
cli_container_name(agent_name)
¶
Return the CLI container name for a bot.
Source code in src/openenv/docker/compose.py
def cli_container_name(agent_name: str) -> str:
"""Return the CLI container name for a bot."""
return f"{slugify_name(agent_name)}-openclaw-cli"
default_compose_filename(agent_name)
¶
Return the default compose filename for a bot.
Source code in src/openenv/docker/compose.py
def default_compose_filename(agent_name: str) -> str:
"""Return the default compose filename for a bot."""
return f"docker-compose-{slugify_name(agent_name)}.yml"
default_env_filename(agent_name)
¶
Return the default env filename for a bot.
Source code in src/openenv/docker/compose.py
def default_env_filename(agent_name: str) -> str:
"""Return the default env filename for a bot."""
return f".{slugify_name(agent_name)}.env"
gateway_container_name(agent_name)
¶
Return the gateway container name for a bot.
Source code in src/openenv/docker/compose.py
def gateway_container_name(agent_name: str) -> str:
"""Return the gateway container name for a bot."""
return f"{slugify_name(agent_name)}-openclaw-gateway"
generate_gateway_token()
¶
Return a random gateway token for generated local stacks.
Source code in src/openenv/docker/compose.py
def generate_gateway_token() -> str:
"""Return a random gateway token for generated local stacks."""
return secrets.token_urlsafe(24)
materialize_runtime_mount_tree(root, manifest, lockfile, *, raw_manifest_text, raw_lock_text)
¶
Write the host-side runtime files expected by the generated bind mounts.
Source code in src/openenv/docker/compose.py
def materialize_runtime_mount_tree(
root: str | Path,
manifest: Manifest,
lockfile: Lockfile,
*,
raw_manifest_text: str,
raw_lock_text: str,
) -> None:
"""Write the host-side runtime files expected by the generated bind mounts."""
root_path = Path(root).resolve()
state_root = root_path / DEFAULT_OPENCLAW_CONFIG_DIR.removeprefix("./")
workspace_root = root_path / DEFAULT_OPENCLAW_WORKSPACE_DIR.removeprefix("./")
payload = render_runtime_payload(
manifest,
lockfile,
raw_manifest_text=raw_manifest_text,
raw_lock_text=raw_lock_text,
)
directories = payload["directories"]
files = payload["files"]
if not isinstance(directories, list) or not isinstance(files, dict):
raise TypeError("Runtime payload shape is invalid.")
for directory in directories:
host_path = _host_mount_path_for_container_path(
str(directory),
manifest,
state_root=state_root,
workspace_root=workspace_root,
)
if host_path is not None:
host_path.mkdir(parents=True, exist_ok=True)
agent_dir = _host_mount_path_for_container_path(
manifest.openclaw.agent_dir(),
manifest,
state_root=state_root,
workspace_root=workspace_root,
)
if agent_dir is not None:
agent_dir.mkdir(parents=True, exist_ok=True)
for container_path, content in sorted(files.items()):
host_path = _host_mount_path_for_container_path(
str(container_path),
manifest,
state_root=state_root,
workspace_root=workspace_root,
)
if host_path is None or not isinstance(content, str):
continue
host_path.parent.mkdir(parents=True, exist_ok=True)
if _should_preserve_existing_catalog_skill_stub(
host_path,
container_path=str(container_path),
placeholder_paths=_catalog_skill_placeholder_paths(manifest),
):
continue
host_path.write_text(content, encoding="utf-8")
prepare_runtime_env_values(existing_values=None)
¶
Fill runtime env values that should exist before writing generated env files.
Source code in src/openenv/docker/compose.py
def prepare_runtime_env_values(existing_values: dict[str, str] | None = None) -> dict[str, str]:
"""Fill runtime env values that should exist before writing generated env files."""
values = dict(existing_values or {})
if not values.get("OPENCLAW_GATEWAY_TOKEN", "").strip():
values["OPENCLAW_GATEWAY_TOKEN"] = generate_gateway_token()
return values
render_all_bots_compose(specs)
¶
Render a shared stack with one gateway and CLI services for all bots.
Source code in src/openenv/docker/compose.py
def render_all_bots_compose(specs: Sequence[AllBotsComposeSpec]) -> str:
"""Render a shared stack with one gateway and CLI services for all bots."""
if not specs:
raise ValueError("At least one bot is required to render the shared compose stack.")
shared_env_file = f"./{all_bots_env_filename()}"
shared_runtime_mount = f"{ALL_BOTS_GATEWAY_ROOT_DIR}:{ALL_BOTS_GATEWAY_CONTAINER_ROOT}"
shared_runtime_user = _shared_runtime_user(specs)
gateway_command = _gateway_startup_command(
[
(
str(PurePosixPath(ALL_BOTS_GATEWAY_CONTAINER_ROOT) / "workspace" / spec.slug),
spec.manifest,
)
for spec in specs
]
)
lines = [
"services:",
f" {ALL_BOTS_GATEWAY_SERVICE}:",
f' image: {_quoted(f"${{OPENCLAW_GATEWAY_IMAGE:-{DEFAULT_OPENCLAW_GATEWAY_IMAGE}}}")}',
f" container_name: {_quoted(ALL_BOTS_GATEWAY_CONTAINER)}",
f" user: {_quoted(shared_runtime_user)}",
" env_file:",
f" - {_quoted(shared_env_file)}",
" environment:",
]
lines.extend(_render_environment(_shared_gateway_environment()))
lines.extend(
[
" cap_drop:",
" - ALL",
]
)
lines.extend(_runtime_capability_lines(shared_runtime_user))
lines.extend(
[
" security_opt:",
" - no-new-privileges:true",
" read_only: true",
" tmpfs:",
f' - "${{OPENCLAW_TMPFS:-{DEFAULT_OPENCLAW_TMPFS}}}"',
f' pids_limit: "${{OPENCLAW_PIDS_LIMIT:-{DEFAULT_OPENCLAW_PIDS_LIMIT}}}"',
" ulimits:",
" nofile:",
f' soft: "${{OPENCLAW_NOFILE_SOFT:-{DEFAULT_OPENCLAW_NOFILE_SOFT}}}"',
f' hard: "${{OPENCLAW_NOFILE_HARD:-{DEFAULT_OPENCLAW_NOFILE_HARD}}}"',
f' nproc: "${{OPENCLAW_NPROC:-{DEFAULT_OPENCLAW_NPROC}}}"',
" volumes:",
f" - {_quoted(shared_runtime_mount)}",
" ports:",
(
f' - "${{OPENCLAW_GATEWAY_HOST_BIND:-{DEFAULT_OPENCLAW_GATEWAY_HOST_BIND}}}'
f':${{OPENCLAW_GATEWAY_PORT:-{DEFAULT_OPENCLAW_GATEWAY_PORT}}}:18789"'
),
(
f' - "${{OPENCLAW_BRIDGE_HOST_BIND:-{DEFAULT_OPENCLAW_BRIDGE_HOST_BIND}}}'
f':${{OPENCLAW_BRIDGE_PORT:-{DEFAULT_OPENCLAW_BRIDGE_PORT}}}:18790"'
),
" init: true",
" restart: unless-stopped",
" command:",
" [",
' "sh",',
' "-lc",',
f" {_quoted(gateway_command)},",
" ]",
" healthcheck:",
" test:",
" [",
' "CMD",',
' "node",',
' "-e",',
' "fetch(\'http://127.0.0.1:18789/healthz\').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",',
" ]",
" interval: 30s",
" timeout: 5s",
" retries: 5",
" start_period: 20s",
]
)
for spec in specs:
service_name = _all_bots_cli_service_name(spec.slug)
container_name = _all_bots_cli_container_name(spec.slug)
env_file = f"./{spec.slug}/{default_env_filename(spec.manifest.openclaw.agent_name)}"
cli_env = dict(_base_service_environment(spec.manifest))
cli_env["HOME"] = ALL_BOTS_GATEWAY_HOME
cli_env["OPENCLAW_CONFIG_PATH"] = ALL_BOTS_GATEWAY_CONFIG_PATH
cli_env["OPENCLAW_STATE_DIR"] = ALL_BOTS_GATEWAY_STATE_DIR
cli_env["BROWSER"] = "echo"
lines.extend(
[
"",
f" {service_name}:",
f" image: {_quoted(spec.image_tag)}",
" build:",
f' context: {_quoted(f"./{spec.slug}")}',
f" dockerfile: {_quoted(DEFAULT_DOCKERFILE_NAME)}",
" args:",
' OPENCLAW_INSTALL_BROWSER: "${OPENCLAW_INSTALL_BROWSER:-}"',
' OPENCLAW_INSTALL_DOCKER_CLI: "${OPENCLAW_INSTALL_DOCKER_CLI:-}"',
f" container_name: {_quoted(container_name)}",
f" user: {_quoted(shared_runtime_user)}",
' network_mode: "service:openclaw-gateway"',
" cap_drop:",
" - ALL",
]
)
lines.extend(_runtime_capability_lines(shared_runtime_user))
lines.extend(
[
" security_opt:",
" - no-new-privileges:true",
" read_only: true",
" tmpfs:",
f' - "${{OPENCLAW_TMPFS:-{DEFAULT_OPENCLAW_TMPFS}}}"',
f' pids_limit: "${{OPENCLAW_PIDS_LIMIT:-{DEFAULT_OPENCLAW_PIDS_LIMIT}}}"',
" ulimits:",
" nofile:",
f' soft: "${{OPENCLAW_NOFILE_SOFT:-{DEFAULT_OPENCLAW_NOFILE_SOFT}}}"',
f' hard: "${{OPENCLAW_NOFILE_HARD:-{DEFAULT_OPENCLAW_NOFILE_HARD}}}"',
f' nproc: "${{OPENCLAW_NPROC:-{DEFAULT_OPENCLAW_NPROC}}}"',
" env_file:",
f" - {_quoted(env_file)}",
f" - {_quoted(shared_env_file)}",
" environment:",
]
)
lines.extend(_render_environment(cli_env))
lines.extend(
[
" volumes:",
f" - {_quoted(shared_runtime_mount)}",
" stdin_open: true",
" tty: true",
" init: true",
f" entrypoint: [{', '.join(_quoted(part) for part in OPENCLAW_HELPER_ENTRYPOINT)}]",
" depends_on:",
f" - {ALL_BOTS_GATEWAY_SERVICE}",
]
)
return "\n".join(lines) + "\n"
render_all_bots_env_file(*, existing_values=None)
¶
Render the shared env file consumed by the all-bots gateway and CLI helpers.
Source code in src/openenv/docker/compose.py
def render_all_bots_env_file(*, existing_values: dict[str, str] | None = None) -> str:
"""Render the shared env file consumed by the all-bots gateway and CLI helpers."""
values = dict(existing_values or {})
used_keys: set[str] = set()
lines = [
"# Shared OpenClaw runtime secrets for the all-bots gateway",
(
"# Use with: docker compose -f "
f"{all_bots_compose_filename()} up -d"
),
"",
]
for key, default in DEFAULT_OPENCLAW_ENV_DEFAULTS:
lines.append(f"{key}={values.get(key, default)}")
used_keys.add(key)
extra_keys = sorted(key for key in values if key not in used_keys)
if extra_keys:
lines.append("")
lines.append("# Preserved custom values")
for key in extra_keys:
lines.append(f"{key}={values[key]}")
return "\n".join(lines).rstrip() + "\n"
render_compose(manifest, image_tag)
¶
Render an OpenClaw-style docker-compose file for the bot image.
Source code in src/openenv/docker/compose.py
def render_compose(manifest: Manifest, image_tag: str) -> str:
"""Render an OpenClaw-style docker-compose file for the bot image."""
env_file = default_env_filename(manifest.openclaw.agent_name)
gateway_name = gateway_container_name(manifest.openclaw.agent_name)
cli_name = cli_container_name(manifest.openclaw.agent_name)
image_ref = f"${{OPENCLAW_IMAGE:-{image_tag}}}"
gateway_command = _gateway_startup_command(
[(manifest.openclaw.workspace, manifest)]
)
config_mount = (
f"${{OPENCLAW_CONFIG_DIR:-{DEFAULT_OPENCLAW_CONFIG_DIR}}}:{manifest.openclaw.state_dir}"
)
workspace_mount = (
f"${{OPENCLAW_WORKSPACE_DIR:-{DEFAULT_OPENCLAW_WORKSPACE_DIR}}}:"
f"{manifest.openclaw.workspace}"
)
gateway_env = _base_service_environment(manifest)
cli_env = dict(gateway_env)
cli_env["BROWSER"] = "echo"
lines = [
"services:",
f" {OPENCLAW_GATEWAY_SERVICE}:",
f" image: {_quoted(image_ref)}",
" build:",
f" context: {_quoted(DEFAULT_BUILD_CONTEXT)}",
f" dockerfile: {_quoted(DEFAULT_DOCKERFILE_NAME)}",
" args:",
' OPENCLAW_INSTALL_BROWSER: "${OPENCLAW_INSTALL_BROWSER:-}"',
' OPENCLAW_INSTALL_DOCKER_CLI: "${OPENCLAW_INSTALL_DOCKER_CLI:-}"',
f" container_name: {_quoted(gateway_name)}",
f" user: {_quoted(_effective_runtime_user(manifest))}",
" env_file:",
f" - {_quoted(env_file)}",
" environment:",
]
lines.extend(_render_environment(gateway_env))
lines.extend(
[
" cap_drop:",
" - ALL",
]
)
lines.extend(_runtime_capability_lines(_effective_runtime_user(manifest)))
lines.extend(
[
" security_opt:",
" - no-new-privileges:true",
" read_only: true",
" tmpfs:",
f' - "${{OPENCLAW_TMPFS:-{DEFAULT_OPENCLAW_TMPFS}}}"',
f' pids_limit: "${{OPENCLAW_PIDS_LIMIT:-{DEFAULT_OPENCLAW_PIDS_LIMIT}}}"',
" ulimits:",
" nofile:",
f' soft: "${{OPENCLAW_NOFILE_SOFT:-{DEFAULT_OPENCLAW_NOFILE_SOFT}}}"',
f' hard: "${{OPENCLAW_NOFILE_HARD:-{DEFAULT_OPENCLAW_NOFILE_HARD}}}"',
f' nproc: "${{OPENCLAW_NPROC:-{DEFAULT_OPENCLAW_NPROC}}}"',
" volumes:",
f" - {_quoted(config_mount)}",
f" - {_quoted(workspace_mount)}",
" ## Uncomment the lines below to enable sandbox isolation",
" ## (agents.defaults.sandbox). Requires Docker CLI in the image",
" ## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use",
" ## scripts/docker/setup.sh with OPENCLAW_SANDBOX=1 for automated setup.",
" ## WARNING: mounting /var/run/docker.sock grants host root-equivalent access.",
" ## Set DOCKER_GID to the host docker group GID before enabling it.",
' # - "/var/run/docker.sock:/var/run/docker.sock"',
" # group_add:",
' # - "${DOCKER_GID:-999}"',
" ports:",
(
f' - "${{OPENCLAW_GATEWAY_HOST_BIND:-{DEFAULT_OPENCLAW_GATEWAY_HOST_BIND}}}'
f':${{OPENCLAW_GATEWAY_PORT:-{DEFAULT_OPENCLAW_GATEWAY_PORT}}}:18789"'
),
(
f' - "${{OPENCLAW_BRIDGE_HOST_BIND:-{DEFAULT_OPENCLAW_BRIDGE_HOST_BIND}}}'
f':${{OPENCLAW_BRIDGE_PORT:-{DEFAULT_OPENCLAW_BRIDGE_PORT}}}:18790"'
),
" init: true",
" restart: unless-stopped",
" command:",
" [",
' "sh",',
' "-lc",',
f" {_quoted(gateway_command)},",
" ]",
" healthcheck:",
" test:",
" [",
' "CMD",',
' "node",',
' "-e",',
' "fetch(\'http://127.0.0.1:18789/healthz\').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",',
" ]",
" interval: 30s",
" timeout: 5s",
" retries: 5",
" start_period: 20s",
"",
f" {OPENCLAW_CLI_SERVICE}:",
f" image: {_quoted(image_ref)}",
f" container_name: {_quoted(cli_name)}",
f" user: {_quoted(_effective_runtime_user(manifest))}",
' network_mode: "service:openclaw-gateway"',
" cap_drop:",
" - ALL",
]
)
lines.extend(_runtime_capability_lines(_effective_runtime_user(manifest)))
lines.extend(
[
" security_opt:",
" - no-new-privileges:true",
" read_only: true",
" tmpfs:",
f' - "${{OPENCLAW_TMPFS:-{DEFAULT_OPENCLAW_TMPFS}}}"',
f' pids_limit: "${{OPENCLAW_PIDS_LIMIT:-{DEFAULT_OPENCLAW_PIDS_LIMIT}}}"',
" ulimits:",
" nofile:",
f' soft: "${{OPENCLAW_NOFILE_SOFT:-{DEFAULT_OPENCLAW_NOFILE_SOFT}}}"',
f' hard: "${{OPENCLAW_NOFILE_HARD:-{DEFAULT_OPENCLAW_NOFILE_HARD}}}"',
f' nproc: "${{OPENCLAW_NPROC:-{DEFAULT_OPENCLAW_NPROC}}}"',
" env_file:",
f" - {_quoted(env_file)}",
" environment:",
]
)
lines.extend(_render_environment(cli_env))
lines.extend(
[
" volumes:",
f" - {_quoted(config_mount)}",
f" - {_quoted(workspace_mount)}",
" stdin_open: true",
" tty: true",
" init: true",
f" entrypoint: [{', '.join(_quoted(part) for part in OPENCLAW_HELPER_ENTRYPOINT)}]",
" depends_on:",
f" - {OPENCLAW_GATEWAY_SERVICE}",
]
)
return "\n".join(lines) + "\n"
render_env_file(manifest, image_tag, *, existing_values=None)
¶
Render the bot-specific env file with OpenClaw defaults and secrets.
Source code in src/openenv/docker/compose.py
def render_env_file(
manifest: Manifest,
image_tag: str,
*,
existing_values: dict[str, str] | None = None,
) -> str:
"""Render the bot-specific env file with OpenClaw defaults and secrets."""
values = dict(existing_values or {})
secret_names = [secret.name for secret in manifest.runtime.secret_refs]
used_keys: set[str] = set()
lines = [
f"# OpenClaw runtime and secrets for {manifest.openclaw.agent_name}",
(
"# Use with: docker compose --env-file "
f"{default_env_filename(manifest.openclaw.agent_name)} -f "
f"{default_compose_filename(manifest.openclaw.agent_name)} up -d"
),
"",
"# OpenClaw runtime overrides",
"# docker compose builds and tags this image from the adjacent Dockerfile",
]
runtime_defaults = {
"OPENCLAW_IMAGE": image_tag,
"OPENCLAW_CONFIG_DIR": DEFAULT_OPENCLAW_CONFIG_DIR,
"OPENCLAW_WORKSPACE_DIR": DEFAULT_OPENCLAW_WORKSPACE_DIR,
"OPENCLAW_GATEWAY_HOST_BIND": DEFAULT_OPENCLAW_GATEWAY_HOST_BIND,
"OPENCLAW_BRIDGE_HOST_BIND": DEFAULT_OPENCLAW_BRIDGE_HOST_BIND,
"OPENCLAW_GATEWAY_PORT": DEFAULT_OPENCLAW_GATEWAY_PORT,
"OPENCLAW_BRIDGE_PORT": DEFAULT_OPENCLAW_BRIDGE_PORT,
"OPENCLAW_GATEWAY_BIND": DEFAULT_OPENCLAW_GATEWAY_BIND,
"OPENCLAW_STATE_DIR": manifest.openclaw.state_dir,
"OPENCLAW_CONFIG_PATH": manifest.openclaw.config_path(),
"OPENCLAW_TMPFS": DEFAULT_OPENCLAW_TMPFS,
"OPENCLAW_PIDS_LIMIT": DEFAULT_OPENCLAW_PIDS_LIMIT,
"OPENCLAW_NOFILE_SOFT": DEFAULT_OPENCLAW_NOFILE_SOFT,
"OPENCLAW_NOFILE_HARD": DEFAULT_OPENCLAW_NOFILE_HARD,
"OPENCLAW_NPROC": DEFAULT_OPENCLAW_NPROC,
"OPENCLAW_TZ": DEFAULT_OPENCLAW_TIMEZONE,
"OPENCLAW_INSTALL_BROWSER": "",
"OPENCLAW_INSTALL_DOCKER_CLI": "",
}
for key, default in runtime_defaults.items():
if key == "OPENCLAW_IMAGE":
current_value = values.get(key)
if current_value == LEGACY_OPENCLAW_IMAGE:
value = default
else:
value = values.get(key, default)
else:
value = values.get(key, default)
lines.append(f"{key}={value}")
used_keys.add(key)
for key, default in DEFAULT_OPENCLAW_ENV_DEFAULTS:
lines.append(f"{key}={values.get(key, default)}")
used_keys.add(key)
advisory_values = {key: values.get(key, default) for key, default in runtime_defaults.items()}
advisory_values.update(
{key: values.get(key, default) for key, default in DEFAULT_OPENCLAW_ENV_DEFAULTS}
)
advisories = assess_runtime_env_security(advisory_values)
if advisories:
lines.append("")
lines.append("# Security advisories for explicit runtime overrides")
for advisory in advisories:
lines.append(f"# WARNING: {advisory}")
if secret_names:
lines.append("")
lines.append("# Bot secret references")
for secret in manifest.runtime.secret_refs:
lines.append("")
lines.append(f"# source: {secret.source}")
lines.append(f"# required: {'true' if secret.required else 'false'}")
lines.append(f"{secret.name}={values.get(secret.name, '')}")
used_keys.add(secret.name)
extra_keys = sorted(key for key in values if key not in used_keys)
if extra_keys:
lines.append("")
lines.append("# Preserved custom values")
for key in extra_keys:
lines.append(f"{key}={values[key]}")
return "\n".join(lines).rstrip() + "\n"
write_compose(path, compose_text)
¶
Write the compose file to disk.
Source code in src/openenv/docker/compose.py
def write_compose(path: str | Path, compose_text: str) -> None:
"""Write the compose file to disk."""
Path(path).write_text(compose_text, encoding="utf-8")
write_env_file(path, env_text)
¶
Write the env file to disk.
Source code in src/openenv/docker/compose.py
def write_env_file(path: str | Path, env_text: str) -> None:
"""Write the env file to disk."""
Path(path).write_text(env_text, encoding="utf-8")
openenv.docker.builder¶
openenv.docker.builder
¶
Docker image build helpers.
build_image(dockerfile_text, tag)
¶
Build a Docker image from a rendered Dockerfile.
Source code in src/openenv/docker/builder.py
def build_image(dockerfile_text: str, tag: str) -> None:
"""Build a Docker image from a rendered Dockerfile."""
build_image_with_args(dockerfile_text, tag, build_args=None)
build_image_with_args(dockerfile_text, tag, *, build_args)
¶
Build a Docker image from a rendered Dockerfile with optional build args.
Source code in src/openenv/docker/builder.py
def build_image_with_args(
dockerfile_text: str,
tag: str,
*,
build_args: dict[str, str] | None,
) -> None:
"""Build a Docker image from a rendered Dockerfile with optional build args."""
with tempfile.TemporaryDirectory(prefix="openclawenv-build-") as temp_dir:
dockerfile_path = Path(temp_dir) / "Dockerfile"
dockerfile_path.write_text(dockerfile_text, encoding="utf-8")
command = [
"docker",
"build",
"--tag",
tag,
"--file",
str(dockerfile_path),
temp_dir,
]
for key, value in sorted((build_args or {}).items()):
command.extend(["--build-arg", f"{key}={value}"])
try:
subprocess.run(command, check=True)
except OSError as exc:
raise CommandError("Docker is not available on PATH.") from exc
except subprocess.CalledProcessError as exc:
raise CommandError(
f"Docker build failed for tag {tag} with exit code {exc.returncode}."
) from exc
default_image_tag(project_name, version)
¶
Compute the default docker tag for a project.
Source code in src/openenv/docker/builder.py
def default_image_tag(project_name: str, version: str) -> str:
"""Compute the default docker tag for a project."""
return f"openclawenv/{slugify_name(project_name)}:{version}"
openenv.docker.runtime¶
openenv.docker.runtime
¶
Runtime inspection helpers for running bot containers.
CapturedSkill
dataclass
¶
A skill snapshot collected from a running container.
Source code in src/openenv/docker/runtime.py
@dataclass(slots=True)
class CapturedSkill:
"""A skill snapshot collected from a running container."""
name: str
description: str
content: str
source: str | None = None
assets: dict[str, str] = field(default_factory=dict)
fetch_container_logs(container_name, *, tail=120)
¶
Return recent logs for a running container.
Source code in src/openenv/docker/runtime.py
def fetch_container_logs(container_name: str, *, tail: int = 120) -> str:
"""Return recent logs for a running container."""
return _run_command(
["docker", "logs", "--tail", str(tail), container_name],
unavailable_message=(
"Docker is not available on PATH. Install Docker or Docker Desktop "
"before reading bot logs."
),
failure_message=f"Failed to read logs for container `{container_name}`.",
)
list_running_container_names()
¶
Return the names of running Docker containers.
Source code in src/openenv/docker/runtime.py
def list_running_container_names() -> set[str]:
"""Return the names of running Docker containers."""
stdout = _run_command(
["docker", "ps", "--format", "{{.Names}}"],
unavailable_message=(
"Docker is not available on PATH. Install Docker or Docker Desktop "
"before listing running bots."
),
failure_message="Failed to list running Docker containers.",
)
return {line.strip() for line in stdout.splitlines() if line.strip()}
snapshot_installed_skills(container_name, *, workspace)
¶
Snapshot installed skills from a running bot container.
Source code in src/openenv/docker/runtime.py
def snapshot_installed_skills(
container_name: str,
*,
workspace: str,
) -> list[CapturedSkill]:
"""Snapshot installed skills from a running bot container."""
stdout = _run_command(
[
"docker",
"exec",
container_name,
"python",
"-c",
SNAPSHOT_SCRIPT_TEMPLATE.format(workspace=workspace),
],
unavailable_message=(
"Docker is not available on PATH. Install Docker or Docker Desktop "
"before creating a skill snapshot."
),
failure_message=f"Failed to snapshot installed skills for `{container_name}`.",
)
try:
payload = json.loads(stdout.strip() or "[]")
except json.JSONDecodeError as exc:
raise CommandError(
f"Container `{container_name}` returned an unreadable skill snapshot payload."
) from exc
if not isinstance(payload, list):
raise CommandError(
f"Container `{container_name}` returned an invalid skill snapshot payload."
)
snapshots: list[CapturedSkill] = []
for item in payload:
if not isinstance(item, dict):
continue
name = str(item.get("name", "")).strip()
files = item.get("files", {})
if not name or not isinstance(files, dict):
continue
skill_md = files.get("SKILL.md")
if not isinstance(skill_md, str) or not skill_md.strip():
continue
assets = {
path: content
for path, content in sorted(files.items())
if path != "SKILL.md" and isinstance(path, str) and isinstance(content, str)
}
frontmatter = _parse_frontmatter(skill_md)
snapshots.append(
CapturedSkill(
name=name,
description=frontmatter.get(
"description",
f"Snapshotted skill from running container {container_name}",
),
content=skill_md,
source=frontmatter.get("source"),
assets=assets,
)
)
return snapshots