mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-18 05:39:50 +08:00
* feat(gateway): add TOML configuration file (RFC 0003)
Introduces an opt-in --config / OPENSHELL_GATEWAY_CONFIG flag that loads a
TOML file with gateway-wide settings and per-driver tables. Source
precedence is CLI > env > file > built-in default, implemented via clap's
ValueSource so existing flags and env vars keep their priority.
Driver crates (kubernetes, docker, podman, vm) now derive Deserialize on
their config structs. SupervisorSideloadMethod gains Deserialize with
kebab-case rename. A per-driver inheritance allowlist on the loader side
overlays [openshell.gateway] shared defaults (default_image,
supervisor_image, image_pull_policy, guest_tls_*, ssh_handshake_skew_secs,
client_tls_secret_name, host_gateway_ip, enable_user_namespaces) onto
each [openshell.drivers.<name>] table before deserialization.
The Helm chart renders a new gateway-config ConfigMap and mounts it at
/etc/openshell/gateway.toml. The migrated OPENSHELL_* env entries are
dropped from the StatefulSet — only the Secret-backed
OPENSHELL_SSH_HANDSHAKE_SECRET remains. database_url stays on --db-url.
Adds examples/gateway/gateway.example.toml and updates architecture/gateway.md
with the source precedence and inheritance rules.
* docs(gateway): drop ssh_handshake_skew_secs and ssh_handshake_secret from examples
Both fields are scheduled for removal. Remove the example values and the
env-only note so the gateway.toml example and the architecture doc stop
recommending settings that will not exist much longer.
* docs(rfc): correct OPENSHELL_CONFIG to OPENSHELL_GATEWAY_CONFIG in RFC 0003
* docs(gateway): add per-driver TOML example configurations
Adds focused single-driver examples next to the comprehensive
gateway.example.toml: kubernetes, docker, podman, and microvm. Each one
demonstrates the realistic settings for that driver plus how shared
[openshell.gateway] defaults inherit into the driver table.
A new unit test (`checked_in_examples_parse`) loads every example through
the config_file loader so schema drift fails CI rather than silently
shipping a broken example.
* refactor(gateway): drop image_pull_policy from shared inheritance
Kubernetes and Podman use mutually-incompatible vocabularies for the same
TOML key:
- Kubernetes: `Always | IfNotPresent | Never` (free-form string passed
verbatim to the K8s API).
- Podman: `always | missing | never | newer` (strict lowercase enum
deserialised into `ImagePullPolicy`).
No value means the same thing in both drivers. Sharing the key at
`[openshell.gateway]` scope and inheriting it into every active driver's
table meant any value safe for one driver was either wrong or silently
dropped for the other (`IfNotPresent` → `ImagePullPolicy::Missing` after
`.unwrap_or_default()`). Operators run one driver per gateway, so the
"shared default" never pays for itself.
Make `image_pull_policy` driver-local:
- Remove the field from `GatewayFileSection` and from
`inheritable_keys()` for both Kubernetes and Podman.
- Drop the file→`RunArgs` merge for the gateway-scope key.
- Stop unconditionally clobbering the driver value with
`config.sandbox_image_pull_policy` in the runtime wiring — only apply
the CLI/env override when it was set (and, for Podman, only when it
parses into the lowercase enum).
- Move the key under `[openshell.drivers.kubernetes]` and
`[openshell.drivers.podman]` in every example, the RFC, the
architecture doc, and the Helm-rendered gateway ConfigMap.
The supervisor pull policy follows the same shape: it is K8s-only and
moves into `[openshell.drivers.kubernetes]` alongside `image_pull_policy`
in the Helm template.
* fix(gateway): address review feedback on TOML configuration
Resolves the P1 and P2 issues raised in PR #1317:
- Helm gateway ConfigMap moves `grpc_endpoint` under
`[openshell.drivers.kubernetes]` so the default install no longer fails
the gateway's `deny_unknown_fields` schema check.
- `kubernetes_config_from_file` and `podman_config_from_file` only let
the gateway-wide CLI/env `grpc_endpoint` overwrite the driver-table
value when it was actually supplied, preserving file-only configs.
- Kubernetes driver default `image_pull_policy` is now empty (was Podman
vocabulary "missing"), so default deployments let the Kubernetes API
apply its own policy instead of being rejected.
- New `disable_tls` gateway field plumbs `.Values.server.disableTls`
through the TOML ConfigMap instead of relying on env vars dropped from
the StatefulSet.
- StatefulSet pod template now carries a `checksum/gateway-config`
annotation so `helm upgrade` rolls pods when the ConfigMap changes.
- Auxiliary listener resolution preserves the full `SocketAddr` from
`health_bind_address` / `metrics_bind_address`, so a loopback-pinned
health port is not silently relocated onto the public bind address.
- `ssh_session_ttl_secs` from the file is now applied to `Config` (it
was previously accepted by the loader but never read).
New regression coverage: cli-level merge tests for the new fields plus
helm-unittest assertions for the ConfigMap shape, checksum annotation,
and `disable_tls` rendering.
* docs(gateway): consolidate gateway TOML examples into docs reference
Replaces the per-driver example files under examples/gateway/ with a
single published reference page at docs/reference/gateway-config.mdx
covering source precedence, layout, the full example, and the four
per-driver examples (Kubernetes, Docker, Podman, microVM). Drew flagged
during PR #1317 review that the examples belong with the user-facing
docs rather than in a sibling examples/ directory.
The cross-references in architecture/gateway.md and RFC 0003 are updated
to point at the new docs page; the round-trip test in config_file.rs is
removed (schema coverage stays on the inline parses_full_example test
and per-field merge tests — doc-snippet drift belongs in a separate
docs-lint, not in a cross-tree Rust unit test).
* refactor(core): move DEFAULT_K8S_NAMESPACE into K8s driver
The constant is Kubernetes-specific (used only by KubernetesComputeConfig's
Default impl) and does not belong in openshell-core. Relocate it to the
driver crate that owns the K8s vocabulary; openshell-core retains only
truly cross-cutting defaults.
* refactor(core): move Podman bridge default into Podman driver
DEFAULT_NETWORK_NAME is Podman vocabulary, consumed only by the Podman
driver. Also drops the unused DEFAULT_IMAGE_PULL_POLICY constant.
* docs(auth): scrub remaining SSH handshake secret references
Sweeps the trailing mentions left after the rebase: the gateway
config-file module doc, the Helm gateway-config ConfigMap header,
the gateway-config.mdx env-only note, and the RPM systemd unit
comment for init-gateway-env.sh.
* docs(gateway): clarify OPENSHELL_GRPC_ENDPOINT applies to all drivers
The previous comment implied the callback endpoint was Kubernetes-only,
but the value is propagated to every compute driver (Kubernetes, Docker,
Podman, VM) and must be reachable from wherever the sandbox runs.
* refactor(gateway): move driver options into config (#1394)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
* fix(e2e): regenerate gateway config via TOML for docker + podman harnesses
The gateway CLI flags moved into TOML config tables in 560550d2 (#1394),
which made every existing e2e/with-{docker,podman}-gateway.sh invocation
fail with "unexpected argument '--sandbox-namespace'" (and a long tail
of similar driver-specific options) before the gateway could even bind.
Replace the obsolete CLI flags with a synthesized
`[openshell.drivers.<driver>]` table written to `${STATE_DIR}/gateway.toml`
and passed via the new `--config` flag. Only the gateway-wide flags
that survived 560550d2 (bind-address, port, drivers, db-url, tls-*,
disable-tls, log-level, health-port) stay on the command line.
Both scripts get a small `toml_string` helper to properly TOML-quote
the values (the previous `%q` printf format produced bash-escape, not
TOML-escape). The Docker harness also corrects two field names that
diverged from the driver schema: `docker_network_name` →
`network_name`, and the supervisor binary/image plumbing now reads
through to `supervisor_bin` / `supervisor_image` in the same table.
The Podman harness drops `--ssh-gateway-port` (deleted in 560550d2 —
gRPC + SSH are multiplexed on the same port now) and substitutes
`network_name` + `gateway_port` for the obsolete `--sandbox-namespace`
(which the Podman driver never had as a typed field).
* fix(core): swap bind-only 0.0.0.0 SSH gateway host for cluster URL host
CLI's resolve_ssh_gateway treated 0.0.0.0 as a loopback "keep as-is"
when the cluster URL was also loopback, so the SSH proxy connected to
0.0.0.0:port. The unspecified address is never a valid connect target
and is not present in any TLS cert SAN, which produced BadCertificate
TLS handshake failures during `openshell sandbox create -- ...` in
docker/podman e2e (e.g. bypass_detection).
Resolution: when the server returns 0.0.0.0 or :: as the gateway host
and both endpoints are loopback, fall back to the cluster URL's host
(which the CLI is already using to reach the gateway, so it must
resolve and match the cert).
* fix(e2e): repair podman harness on macOS
Podman 5.x with the applehv/libkrun provider no longer creates the legacy
~/.local/share/containers/podman/machine/podman.sock symlink, and
`podman system service` is a Linux-only subcommand — the macOS client
delegates the API service to the VM. Both assumptions in the harness
were stale, so the script tried to start a temporary service that podman
rejected with "unknown flag: --time".
- Discover the macOS socket via `podman machine inspect` instead of the
hardcoded path.
- On Darwin, fail fast with a "start podman machine" message rather than
attempting the Linux-only `podman system service` fallback.
- Write socket_path into [openshell.drivers.podman] so the in-process
driver picks up the discovered socket; the driver reads TOML only
after the config refactor (560550d2), so OPENSHELL_PODMAN_SOCKET alone
was no longer enough.
* fix(server): use clone_from for TLS client CA assignment
clippy 1.95.0 rejects assigning the result of `Clone::clone()` to an
existing variable under `-D warnings` (`assigning_clones`). Switch to
`clone_from(&...)` to satisfy the lint and avoid the redundant
allocation.
---------
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Co-authored-by: Drew Newberry <anewberry@nvidia.com>
528 lines
16 KiB
Python
528 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
SEMVER_TAG_GLOB = "v[0-9]*.[0-9]*.[0-9]*"
|
|
SEMVER_TAG_RE = re.compile(r"^v?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)$")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Versions:
|
|
python: str
|
|
cargo: str
|
|
docker: str
|
|
deb: str
|
|
snap: str
|
|
rpm_version: str
|
|
rpm_release: str
|
|
git_tag: str
|
|
git_sha: str
|
|
git_distance: int
|
|
|
|
|
|
HOMEBREW_TARGET = "aarch64-apple-darwin"
|
|
HOMEBREW_CLI_ASSET = f"openshell-{HOMEBREW_TARGET}.tar.gz"
|
|
HOMEBREW_GATEWAY_ASSET = f"openshell-gateway-{HOMEBREW_TARGET}.tar.gz"
|
|
HOMEBREW_DRIVER_VM_ASSET = f"openshell-driver-vm-{HOMEBREW_TARGET}.tar.gz"
|
|
GITHUB_RELEASE_DOWNLOADS = "https://github.com/NVIDIA/OpenShell/releases/download"
|
|
LOCAL_GATEWAY_PORT = 17670
|
|
_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
|
_RELEASE_TAG_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
return Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def _run(cmd: list[str], *, env: dict[str, str] | None = None) -> None:
|
|
subprocess.run(cmd, check=True, env=env)
|
|
|
|
|
|
def _git(cmd: list[str]) -> str:
|
|
return (
|
|
subprocess.check_output(["git", *cmd], cwd=_repo_root()).decode("utf-8").strip()
|
|
)
|
|
|
|
|
|
def _parse_semver_tag(tag: str) -> tuple[int, int, int] | None:
|
|
match = SEMVER_TAG_RE.match(tag)
|
|
if match is None:
|
|
return None
|
|
return (
|
|
int(match.group("major")),
|
|
int(match.group("minor")),
|
|
int(match.group("patch")),
|
|
)
|
|
|
|
|
|
def _format_semver(version: tuple[int, int, int]) -> str:
|
|
return f"{version[0]}.{version[1]}.{version[2]}"
|
|
|
|
|
|
def _next_patch(version: tuple[int, int, int]) -> tuple[int, int, int]:
|
|
return version[0], version[1], version[2] + 1
|
|
|
|
|
|
def _latest_semver_tag() -> str | None:
|
|
try:
|
|
tag = _git(
|
|
["describe", "--tags", "--match", SEMVER_TAG_GLOB, "--abbrev=0", "HEAD"]
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
return None
|
|
|
|
if _parse_semver_tag(tag) is None:
|
|
raise RuntimeError(f"git describe returned non-semver release tag: {tag}")
|
|
return tag
|
|
|
|
|
|
def _versions_from_parts(
|
|
base_version: tuple[int, int, int],
|
|
git_distance: int,
|
|
git_sha: str,
|
|
git_tag: str,
|
|
) -> Versions:
|
|
if git_distance == 0:
|
|
python_version = _format_semver(base_version)
|
|
rpm_version = python_version
|
|
rpm_release = "1"
|
|
else:
|
|
next_version = _format_semver(_next_patch(base_version))
|
|
python_version = f"{next_version}.dev{git_distance}+g{git_sha}"
|
|
rpm_version = next_version
|
|
rpm_release = f"0.dev.{git_distance}.g{git_sha}"
|
|
|
|
# Convert PEP 440 to a SemVer-ish string for Cargo:
|
|
# 0.1.0.dev3+gabcdef -> 0.1.0-dev.3+gabcdef
|
|
cargo_version = re.sub(r"\.dev(\d+)", r"-dev.\1", python_version)
|
|
|
|
# Docker tags can't contain '+'.
|
|
docker_version = cargo_version.replace("+", "-")
|
|
|
|
# Snap versions cannot contain '+' and are limited to 32 characters.
|
|
snap_version = re.sub(r"\.d\d{8}$", "", docker_version)
|
|
if len(snap_version) > 32:
|
|
raise ValueError(f"snap version must be at most 32 characters: {snap_version}")
|
|
|
|
# Debian versions use '~' so prereleases sort before the eventual release.
|
|
deb_version = cargo_version
|
|
deb_version = deb_version[1:] if deb_version.startswith("v") else deb_version
|
|
deb_version = deb_version.replace("-dev.", "~dev.", 1)
|
|
deb_version = f"{deb_version}-1"
|
|
|
|
return Versions(
|
|
python=python_version,
|
|
cargo=cargo_version,
|
|
docker=docker_version,
|
|
deb=deb_version,
|
|
snap=snap_version,
|
|
rpm_version=rpm_version,
|
|
rpm_release=rpm_release,
|
|
git_tag=git_tag,
|
|
git_sha=git_sha,
|
|
git_distance=git_distance,
|
|
)
|
|
|
|
|
|
def _compute_versions() -> Versions:
|
|
git_tag = _latest_semver_tag()
|
|
git_sha = _git(["rev-parse", "--short=9", "HEAD"])
|
|
|
|
if git_tag is None:
|
|
base_version = (0, 0, 0)
|
|
git_distance = int(_git(["rev-list", "--count", "HEAD"]))
|
|
return _versions_from_parts(base_version, git_distance, git_sha, "")
|
|
|
|
parsed_tag = _parse_semver_tag(git_tag)
|
|
if parsed_tag is None:
|
|
raise RuntimeError(f"invalid semantic release tag: {git_tag}")
|
|
|
|
git_distance = int(_git(["rev-list", f"{git_tag}..HEAD", "--count"]))
|
|
return _versions_from_parts(parsed_tag, git_distance, git_sha, git_tag)
|
|
|
|
|
|
def _print_env(versions: Versions) -> None:
|
|
print(f"VERSION_PY={versions.python}")
|
|
print(f"VERSION_CARGO={versions.cargo}")
|
|
print(f"VERSION_DOCKER={versions.docker}")
|
|
print(f"VERSION_DEB={versions.deb}")
|
|
print(f"VERSION_SNAP={versions.snap}")
|
|
print(f"VERSION_RPM={versions.rpm_version}")
|
|
print(f"VERSION_RPM_RELEASE={versions.rpm_release}")
|
|
print(f"GIT_TAG={versions.git_tag}")
|
|
print(f"GIT_SHA={versions.git_sha}")
|
|
print(f"GIT_DISTANCE={versions.git_distance}")
|
|
|
|
|
|
def get_version(format: str) -> None:
|
|
versions = _compute_versions()
|
|
if format == "python":
|
|
print(versions.python)
|
|
elif format == "cargo":
|
|
print(versions.cargo)
|
|
elif format == "docker":
|
|
print(versions.docker)
|
|
elif format == "deb":
|
|
print(versions.deb)
|
|
elif format == "snap":
|
|
print(versions.snap)
|
|
elif format == "rpm-version":
|
|
print(versions.rpm_version)
|
|
elif format == "rpm-release":
|
|
print(versions.rpm_release)
|
|
elif format == "json":
|
|
print(json.dumps(asdict(versions), sort_keys=True))
|
|
else:
|
|
_print_env(versions)
|
|
|
|
|
|
def _parse_sha256_file(path: Path) -> dict[str, str]:
|
|
checksums: dict[str, str] = {}
|
|
for line_number, line in enumerate(
|
|
path.read_text(encoding="utf-8").splitlines(), 1
|
|
):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
parts = line.split()
|
|
if len(parts) < 2:
|
|
raise ValueError(f"{path}:{line_number}: malformed checksum line")
|
|
|
|
digest = parts[0].lower()
|
|
if not _SHA256_RE.fullmatch(digest):
|
|
raise ValueError(f"{path}:{line_number}: invalid SHA-256 digest")
|
|
|
|
filename = parts[1].lstrip("*")
|
|
checksums[filename] = digest
|
|
|
|
return checksums
|
|
|
|
|
|
def _required_checksum(
|
|
checksums: dict[str, str],
|
|
filename: str,
|
|
checksum_path: Path,
|
|
) -> str:
|
|
try:
|
|
return checksums[filename]
|
|
except KeyError as exc:
|
|
raise ValueError(f"{checksum_path}: missing checksum for {filename}") from exc
|
|
|
|
|
|
def _asset_url(release_tag: str, filename: str) -> str:
|
|
return f"{GITHUB_RELEASE_DOWNLOADS}/{release_tag}/{filename}"
|
|
|
|
|
|
def _homebrew_supervisor_image(release_tag: str) -> str:
|
|
image_tag = "dev" if release_tag == "dev" else release_tag.removeprefix("v")
|
|
return f"ghcr.io/nvidia/openshell/supervisor:{image_tag}"
|
|
|
|
|
|
def render_homebrew_formula(
|
|
*,
|
|
release_tag: str,
|
|
cli_sha256: str,
|
|
gateway_sha256: str,
|
|
driver_vm_sha256: str,
|
|
) -> str:
|
|
if not _RELEASE_TAG_RE.fullmatch(release_tag):
|
|
raise ValueError(f"release tag contains unsupported characters: {release_tag}")
|
|
|
|
version = release_tag.removeprefix("v")
|
|
docker_supervisor_image = _homebrew_supervisor_image(release_tag)
|
|
return f"""# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Generated by tasks/scripts/release.py. Do not edit by hand.
|
|
|
|
class Openshell < Formula
|
|
desc "Safe, private runtime for autonomous AI agents"
|
|
homepage "https://github.com/NVIDIA/OpenShell"
|
|
url "{_asset_url(release_tag, HOMEBREW_CLI_ASSET)}"
|
|
sha256 "{cli_sha256}"
|
|
version "{version}"
|
|
license "Apache-2.0"
|
|
|
|
depends_on macos: :big_sur
|
|
depends_on arch: :arm64
|
|
|
|
resource "openshell-gateway" do
|
|
url "{_asset_url(release_tag, HOMEBREW_GATEWAY_ASSET)}"
|
|
sha256 "{gateway_sha256}"
|
|
end
|
|
|
|
resource "openshell-driver-vm" do
|
|
url "{_asset_url(release_tag, HOMEBREW_DRIVER_VM_ASSET)}"
|
|
sha256 "{driver_vm_sha256}"
|
|
end
|
|
|
|
def install
|
|
odie "OpenShell Homebrew formula currently supports macOS only" unless OS.mac?
|
|
|
|
bin.install "openshell"
|
|
|
|
resource("openshell-gateway").stage do
|
|
bin.install "openshell-gateway"
|
|
end
|
|
|
|
resource("openshell-driver-vm").stage do
|
|
libexec.install "openshell-driver-vm"
|
|
end
|
|
|
|
(libexec/"openshell-gateway-homebrew-service").write <<~SH
|
|
#!/bin/sh
|
|
set -eu
|
|
|
|
if [ -z "${{HOME:-}}" ]; then
|
|
echo "HOME must be set for Docker TLS bind mounts" >&2
|
|
exit 1
|
|
fi
|
|
|
|
gateway_env="#{{var}}/openshell/gateway.env"
|
|
if [ -f "${{gateway_env}}" ]; then
|
|
set -a
|
|
. "${{gateway_env}}"
|
|
set +a
|
|
fi
|
|
|
|
docker_tls_dir="${{OPENSHELL_DOCKER_TLS_DIR:-${{HOME}}/.local/state/openshell/homebrew/tls}}"
|
|
mkdir -p "${{docker_tls_dir}}/client"
|
|
chmod 700 "${{docker_tls_dir}}" "${{docker_tls_dir}}/client"
|
|
/usr/bin/install -m 0644 "#{{var}}/openshell/tls/ca.crt" "${{docker_tls_dir}}/ca.crt"
|
|
/usr/bin/install -m 0644 "#{{var}}/openshell/tls/client/tls.crt" "${{docker_tls_dir}}/client/tls.crt"
|
|
/usr/bin/install -m 0600 "#{{var}}/openshell/tls/client/tls.key" "${{docker_tls_dir}}/client/tls.key"
|
|
|
|
gateway_config="${{OPENSHELL_GATEWAY_CONFIG:-#{{var}}/openshell/gateway.toml}}"
|
|
if [ ! -f "${{gateway_config}}" ]; then
|
|
mkdir -p "$(dirname "${{gateway_config}}")" "#{{var}}/openshell/vm-driver"
|
|
cat > "${{gateway_config}}" <<EOF
|
|
[openshell]
|
|
version = 1
|
|
|
|
[openshell.gateway]
|
|
default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"
|
|
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest"
|
|
guest_tls_ca = "#{{var}}/openshell/tls/ca.crt"
|
|
guest_tls_cert = "#{{var}}/openshell/tls/client/tls.crt"
|
|
guest_tls_key = "#{{var}}/openshell/tls/client/tls.key"
|
|
|
|
[openshell.drivers.vm]
|
|
state_dir = "#{{var}}/openshell/vm-driver"
|
|
driver_dir = "#{{opt_libexec}}"
|
|
grpc_endpoint = "https://127.0.0.1:{LOCAL_GATEWAY_PORT}"
|
|
|
|
[openshell.drivers.docker]
|
|
grpc_endpoint = "https://127.0.0.1:{LOCAL_GATEWAY_PORT}"
|
|
supervisor_image = "{docker_supervisor_image}"
|
|
guest_tls_ca = "${{docker_tls_dir}}/ca.crt"
|
|
guest_tls_cert = "${{docker_tls_dir}}/client/tls.crt"
|
|
guest_tls_key = "${{docker_tls_dir}}/client/tls.key"
|
|
EOF
|
|
chmod 0600 "${{gateway_config}}"
|
|
fi
|
|
|
|
export OPENSHELL_GATEWAY_CONFIG="${{gateway_config}}"
|
|
exec "#{{opt_bin}}/openshell-gateway"
|
|
SH
|
|
chmod 0755, libexec/"openshell-gateway-homebrew-service"
|
|
end
|
|
|
|
def post_install
|
|
(var/"openshell/gateway").mkpath
|
|
(var/"openshell/vm-driver").mkpath
|
|
(var/"log/openshell").mkpath
|
|
system bin/"openshell-gateway", "generate-certs", "--output-dir", var/"openshell/tls", "--server-san", "host.openshell.internal"
|
|
|
|
gateway_env = var/"openshell/gateway.env"
|
|
unless gateway_env.exist?
|
|
gateway_env.atomic_write <<~ENV
|
|
# OpenShell Gateway Environment Configuration
|
|
# Edit freely; this file is not overwritten.
|
|
#
|
|
# Uncomment to force the VM compute driver. Leaving this unset keeps
|
|
# normal Podman/Docker/Kubernetes auto-detection.
|
|
#OPENSHELL_DRIVERS=vm
|
|
|
|
# Driver implementation settings live in gateway.toml.
|
|
#OPENSHELL_GATEWAY_CONFIG=#{{var}}/openshell/gateway.toml
|
|
ENV
|
|
chmod 0600, gateway_env
|
|
end
|
|
|
|
entitlements = var/"openshell/openshell-driver-vm.entitlements.plist"
|
|
entitlements.atomic_write <<~XML
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>com.apple.security.hypervisor</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
XML
|
|
|
|
system "/usr/bin/codesign", "--entitlements", entitlements, "--force", "-s", "-", libexec/"openshell-driver-vm"
|
|
end
|
|
|
|
service do
|
|
run opt_libexec/"openshell-gateway-homebrew-service"
|
|
environment_variables(
|
|
OPENSHELL_BIND_ADDRESS: "127.0.0.1",
|
|
OPENSHELL_SERVER_PORT: "{LOCAL_GATEWAY_PORT}",
|
|
OPENSHELL_TLS_CERT: "#{{var}}/openshell/tls/server/tls.crt",
|
|
OPENSHELL_TLS_KEY: "#{{var}}/openshell/tls/server/tls.key",
|
|
OPENSHELL_TLS_CLIENT_CA: "#{{var}}/openshell/tls/ca.crt",
|
|
OPENSHELL_DB_URL: "sqlite:#{{var}}/openshell/gateway/openshell.db",
|
|
OPENSHELL_GATEWAY_CONFIG: "#{{var}}/openshell/gateway.toml",
|
|
)
|
|
keep_alive successful_exit: false
|
|
log_path var/"log/openshell/openshell-gateway.out.log"
|
|
error_log_path var/"log/openshell/openshell-gateway.err.log"
|
|
end
|
|
|
|
def caveats
|
|
<<~EOS
|
|
Start or restart the local gateway with:
|
|
brew services restart openshell
|
|
|
|
Register it with the OpenShell CLI:
|
|
openshell gateway add https://127.0.0.1:{LOCAL_GATEWAY_PORT} --local --name openshell
|
|
EOS
|
|
end
|
|
|
|
test do
|
|
assert_match "openshell ", shell_output("#{{bin}}/openshell --version")
|
|
end
|
|
end
|
|
"""
|
|
|
|
|
|
def generate_homebrew_formula(
|
|
*,
|
|
release_tag: str,
|
|
release_dir: Path,
|
|
output: Path,
|
|
) -> None:
|
|
checksums_path = release_dir / "openshell-checksums-sha256.txt"
|
|
gateway_checksums_path = release_dir / "openshell-gateway-checksums-sha256.txt"
|
|
checksums = _parse_sha256_file(checksums_path)
|
|
gateway_checksums = _parse_sha256_file(gateway_checksums_path)
|
|
|
|
formula = render_homebrew_formula(
|
|
release_tag=release_tag,
|
|
cli_sha256=_required_checksum(checksums, HOMEBREW_CLI_ASSET, checksums_path),
|
|
gateway_sha256=_required_checksum(
|
|
gateway_checksums,
|
|
HOMEBREW_GATEWAY_ASSET,
|
|
gateway_checksums_path,
|
|
),
|
|
driver_vm_sha256=_required_checksum(
|
|
checksums,
|
|
HOMEBREW_DRIVER_VM_ASSET,
|
|
checksums_path,
|
|
),
|
|
)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(formula, encoding="utf-8")
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="OpenClaw release tooling.")
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
get_version_parser = sub.add_parser("get-version", help="Print computed version.")
|
|
get_version_parser.add_argument(
|
|
"--python", action="store_true", help="Print Python version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--cargo", action="store_true", help="Print Cargo version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--docker", action="store_true", help="Print Docker version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--deb", action="store_true", help="Print Debian package version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--snap", action="store_true", help="Print Snap package version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--rpm-version", action="store_true", help="Print RPM Version only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--rpm-release", action="store_true", help="Print RPM Release only."
|
|
)
|
|
get_version_parser.add_argument(
|
|
"--json", action="store_true", help="Print all versions as JSON."
|
|
)
|
|
|
|
formula_parser = sub.add_parser(
|
|
"generate-homebrew-formula",
|
|
help="Generate the per-release Homebrew formula asset.",
|
|
)
|
|
formula_parser.add_argument(
|
|
"--release-tag",
|
|
required=True,
|
|
help="GitHub release tag that owns the formula assets.",
|
|
)
|
|
formula_parser.add_argument(
|
|
"--release-dir",
|
|
type=Path,
|
|
required=True,
|
|
help="Directory containing release artifacts and checksum files.",
|
|
)
|
|
formula_parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
required=True,
|
|
help="Path to write the generated Formula Ruby file.",
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "get-version":
|
|
if args.python:
|
|
get_version("python")
|
|
elif args.cargo:
|
|
get_version("cargo")
|
|
elif args.docker:
|
|
get_version("docker")
|
|
elif args.deb:
|
|
get_version("deb")
|
|
elif args.snap:
|
|
get_version("snap")
|
|
elif args.rpm_version:
|
|
get_version("rpm-version")
|
|
elif args.rpm_release:
|
|
get_version("rpm-release")
|
|
elif args.json:
|
|
get_version("json")
|
|
else:
|
|
get_version("all")
|
|
elif args.command == "generate-homebrew-formula":
|
|
generate_homebrew_formula(
|
|
release_tag=args.release_tag,
|
|
release_dir=args.release_dir,
|
|
output=args.output,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|