diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37750e9..41c6b12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,9 +19,10 @@ jobs: - run: sudo apt-get update && sudo apt-get install --yes musl-tools - run: cargo build --release --locked --target x86_64-unknown-linux-musl - run: | - mkdir -p dist + mkdir -p dist/skills install -m 755 target/x86_64-unknown-linux-musl/release/netsandbox dist/netsandbox - tar -czf netsandbox-linux-x86_64.tar.gz -C dist netsandbox + cp -R skills/network-sandbox dist/skills/network-sandbox + tar -czf netsandbox-linux-x86_64.tar.gz -C dist netsandbox skills/network-sandbox shasum -a 256 netsandbox-linux-x86_64.tar.gz > netsandbox-linux-x86_64.tar.gz.sha256 - uses: actions/upload-artifact@v7 with: @@ -39,9 +40,10 @@ jobs: targets: aarch64-apple-darwin - run: cargo build --release --locked --target aarch64-apple-darwin - run: | - mkdir -p dist + mkdir -p dist/skills install -m 755 target/aarch64-apple-darwin/release/netsandbox dist/netsandbox - tar -czf netsandbox-macos-arm64.tar.gz -C dist netsandbox + cp -R skills/network-sandbox dist/skills/network-sandbox + tar -czf netsandbox-macos-arm64.tar.gz -C dist netsandbox skills/network-sandbox shasum -a 256 netsandbox-macos-arm64.tar.gz > netsandbox-macos-arm64.tar.gz.sha256 - uses: actions/upload-artifact@v7 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index a9362c7..60e3f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.6.2 — 2026-07-28 + +- Bundle the Network Sandbox agent skill with every release archive. +- Add one-command Homebrew installation for both the CLI and skill. +- Replace the Python preflight dependency with direct CLI checks. +- Add a concise Anaconda-style quick start and documented check exit codes. + ## 0.6.1 — 2026-07-28 - Refresh required circuits after administrator authorization and before the diff --git a/Cargo.lock b/Cargo.lock index 43e4963..96728c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -464,7 +464,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "netsandbox" -version = "0.6.1" +version = "0.6.2" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 65673e3..866720d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "netsandbox" -version = "0.6.1" +version = "0.6.2" edition = "2024" rust-version = "1.85" build = "build.rs" diff --git a/README.md b/README.md index 5d7df2b..949ac40 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,45 @@ alone with the mess. It gives agents an isolated place to validate configuration changes before transactionally applying them to the real environment. -## Download +## Install -- [macOS — Apple silicon](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/netsandbox-macos-arm64.tar.gz) -- [Linux — x86_64](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/netsandbox-linux-x86_64.tar.gz) -- [SHA-256 checksums](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/SHA256SUMS) +```bash +brew install MildyNora/tap/network-sandbox +``` -Each archive contains one ready-to-run `netsandbox` executable. No source build, -Rust toolchain, Docker, or virtual machine is required. +This installs both the `netsandbox` CLI and its agent skill. The skill is linked +into the standard Codex and agent skill directories, so agents automatically +follow the protected connectivity workflow. + +No source build, Rust toolchain, Docker, or virtual machine is required. + +Direct packages: [macOS — Apple silicon](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/netsandbox-macos-arm64.tar.gz) +· [Linux — x86_64](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/netsandbox-linux-x86_64.tar.gz) +· [checksums](https://github.com/MildyNora/NetworkSandbox/releases/latest/download/SHA256SUMS) + +## Quick start + +Think of Network Sandbox as Anaconda environments for risky connectivity +changes. An agent creates a named environment, rehearses its commands there, +checks the required connections, reviews the plan, and only then applies the +validated difference: + +```text +create → exec → check → plan → apply +``` + +```bash +netsandbox create proxy-change +netsandbox exec proxy-change -- CHANGE_COMMAND +netsandbox check proxy-change +netsandbox plan proxy-change +sudo netsandbox apply proxy-change --yes +``` + +The real host stays unchanged until `apply`. `check` names every failed or +unverifiable connection and exits with `0` when required circuits are preserved, +`2` when connectivity validation blocks the change, or `1` for an operational +error. Post-apply validation automatically rolls back a failed change. Technical and safety details are available in the [reference documentation](docs/reference.md). diff --git a/docs/reference.md b/docs/reference.md index 44bbd67..024cab4 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -107,9 +107,8 @@ Linux container image. Docker supplies a persistent copy-on-write layer and a private Linux network stack. The source image and Mac host are never mounted writable. -Official macOS release builds embed their matching Linux helper in the main -executable. For a development build without an embedded helper, place both -binaries beside one another: +The optional legacy Linux-image backend requires a matching Linux helper beside +the main executable: ```text /usr/local/bin/netsandbox diff --git a/skills/network-sandbox/SKILL.md b/skills/network-sandbox/SKILL.md index d2b3b64..f5c5260 100644 --- a/skills/network-sandbox/SKILL.md +++ b/skills/network-sandbox/SKILL.md @@ -13,15 +13,16 @@ Treat the active agent connection as a required control circuit. ## Start with preflight -Run the bundled read-only preflight: +Run the dependency-free read-only preflight: ```bash -python3 /scripts/preflight.py +netsandbox --version +netsandbox doctor ``` -If it cannot find `netsandbox`, ask the user to install the binary. Do not replace it with direct -network mutations. If `doctor` reports an unsupported backend, continue read-only diagnosis and -stop before mutation. +If the shell cannot find `netsandbox`, ask the user to install the package. Do not replace it with +direct network mutations. If `doctor` reports an unsupported backend, continue read-only diagnosis +and stop before mutation. `netsandbox` owns this workflow and does not require the legacy NCS helper, `ncsd`, or `/var/run/ncsd.sock`. Do not invoke or wait for NCS unless the user explicitly requests the diff --git a/skills/network-sandbox/references/commands.md b/skills/network-sandbox/references/commands.md index e98c747..15d7176 100644 --- a/skills/network-sandbox/references/commands.md +++ b/skills/network-sandbox/references/commands.md @@ -121,8 +121,8 @@ netsandbox mac linux-diff NAME netsandbox mac linux-commit NAME OUTPUT_IMAGE --yes ``` -Official macOS releases embed the Linux helper. Development builds can discover -`netsandbox-linux-guest` beside `netsandbox`. Override either only when necessary with +The optional legacy Linux-image backend requires a compatible +`netsandbox-linux-guest` beside `netsandbox`. Override discovery only when necessary with `NETSANDBOX_LINUX_GUEST_BIN` or `--guest-binary PATH`. Track files before mutation. Tracking an already changed path is rejected. `linux-diff` shows the diff --git a/skills/network-sandbox/scripts/preflight.py b/skills/network-sandbox/scripts/preflight.py deleted file mode 100755 index 8ed7aa6..0000000 --- a/skills/network-sandbox/scripts/preflight.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -"""Read-only Network Sandbox installation and backend preflight.""" - -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path -import re -import shutil -import subprocess -import sys - -MINIMUM_VERSION = (0, 6, 1) - - -def find_binary(explicit: str | None) -> Path | None: - candidates: list[Path] = [] - if explicit: - candidates.append(Path(explicit).expanduser()) - discovered = shutil.which("netsandbox") - if discovered: - candidates.append(Path(discovered)) - candidates.append(Path.home() / ".local" / "bin" / "netsandbox") - repository = Path(__file__).resolve().parents[3] - candidates.append(repository / "target" / "release" / "netsandbox") - for candidate in candidates: - if candidate.is_file() and os.access(candidate, os.X_OK): - return candidate.resolve() - return None - - -def run(binary: Path, *arguments: str) -> tuple[int, str, str]: - completed = subprocess.run( - [str(binary), *arguments], - check=False, - capture_output=True, - text=True, - timeout=20, - ) - return completed.returncode, completed.stdout.strip(), completed.stderr.strip() - - -def supported_version(output: str) -> bool: - match = re.fullmatch(r"netsandbox (\d+)\.(\d+)\.(\d+)", output.strip()) - return match is not None and tuple(map(int, match.groups())) >= MINIMUM_VERSION - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--binary", help="Explicit netsandbox executable") - parser.add_argument("--json", action="store_true", help="Emit machine-readable output") - arguments = parser.parse_args() - - binary = find_binary(arguments.binary) - if binary is None: - result = { - "available": False, - "reason": "netsandbox executable was not found", - } - if arguments.json: - print(json.dumps(result, indent=2)) - else: - print("FAIL netsandbox executable was not found") - return 2 - - version_code, version, version_error = run(binary, "--version") - doctor_code, doctor, doctor_error = run(binary, "doctor") - version_supported = version_code == 0 and supported_version(version) - result = { - "available": version_code == 0, - "binary": str(binary), - "version": version or version_error, - "minimumVersion": ".".join(map(str, MINIMUM_VERSION)), - "versionSupported": version_supported, - "doctorExit": doctor_code, - "doctor": doctor, - "doctorError": doctor_error, - "ready": version_supported and doctor_code == 0, - } - if arguments.json: - print(json.dumps(result, indent=2)) - else: - print(f"{'PASS' if version_supported else 'FAIL'} {result['version']}") - if version_code == 0 and not version_supported: - print(f" Network Sandbox {result['minimumVersion']} or newer is required") - if doctor: - print(doctor) - if doctor_error: - print(doctor_error, file=sys.stderr) - print(f"{'PASS' if result['ready'] else 'FAIL'} backend preflight") - return 0 if result["ready"] else 2 - - -if __name__ == "__main__": - raise SystemExit(main())