Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
86109c6
add macos support via sandbox-exec
adrian-gierakowski Feb 4, 2026
04ea4c2
improve darwin impl (sandbox-exec) to pass tests added in 635ebfb
adrian-gierakowski Feb 4, 2026
ca9ca02
sandbox-exec impl: clean up and add comments
adrian-gierakowski Feb 4, 2026
178550f
sandbox-exec fix tty
adrian-gierakowski Feb 4, 2026
5860fc5
fix tty test in nix sandbox
adrian-gierakowski Feb 4, 2026
767ae07
fix indentation
adrian-gierakowski Feb 4, 2026
03a2271
only include glibc on linux
adrian-gierakowski Feb 4, 2026
bd59345
remove flake-parts debug = true
adrian-gierakowski Feb 4, 2026
552f39a
test ls with /etc
adrian-gierakowski Feb 4, 2026
619a5c9
use pkgs.perl
adrian-gierakowski Feb 4, 2026
b656502
fix missing USER
adrian-gierakowski Feb 4, 2026
bfa7629
further cleanup and add test features.tmp
adrian-gierakowski Feb 4, 2026
47fb1cb
allow exec from tmp
adrian-gierakowski Feb 5, 2026
2014598
Refactor Darwin implementation into its own directory
google-labs-jules[bot] Feb 13, 2026
1ecbf4b
create linux, darwin and common dirs to seperate platform dependant code
adrian-gierakowski Feb 23, 2026
8ba32c2
vira: add macOS
srid Feb 26, 2026
f78c1b3
ci: run tests on github actions
srid Feb 27, 2026
fbd88dd
oi
srid Feb 27, 2026
b12e3b9
ci: use nixbuild/nix-quick-install-action v34
adrian-gierakowski Mar 4, 2026
b4ccd84
ci: use cachix/install-nix-action@v31
adrian-gierakowski Mar 4, 2026
44e3245
ci: fix TTY tests by providing a pseudo-terminal (PTY) via script
adrian-gierakowski Mar 4, 2026
9fba4a9
Revert "ci: use cachix/install-nix-action@v31"
adrian-gierakowski Mar 4, 2026
9c441e0
Address PR #18 review comments
google-labs-jules[bot] Mar 5, 2026
82438fc
update example claude-sandboxed to use parent flake
adrian-gierakowski Mar 5, 2026
f6b9b9b
fix mktemp sandbox-profile on macos
adrian-gierakowski Mar 5, 2026
455179d
use coreutils mktemp in modules/flake-parts/landrun/darwin/wrapper.nix
adrian-gierakowski Mar 5, 2026
b1c3408
add coreutils to runtimeInputs of modules/flake-parts/landrun/darwin/…
adrian-gierakowski Mar 5, 2026
bd596a4
allow setting `name` different than submodule.name
adrian-gierakowski Mar 5, 2026
905091f
add meta.mainProgram to wrapper
adrian-gierakowski Mar 5, 2026
aac4746
use lib.getExe to get bin path of langrunApp when mapping to flake apps
adrian-gierakowski Mar 5, 2026
b4444e9
Revert "add meta.mainProgram to wrapper"
adrian-gierakowski Mar 5, 2026
664d588
improve env var handling on darwin
google-labs-jules[bot] Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Tests

on:
push:
branches: [main, master]
pull_request:

jobs:
test:
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install Nix
uses: nixbuild/nix-quick-install-action@v34

- name: Install just
uses: extractions/setup-just@v2

- name: Run tests
run: just test
16 changes: 6 additions & 10 deletions examples/claude-sandboxed/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/claude-sandboxed/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
landrun-nix.url = "github:srid/landrun-nix";
landrun-nix.url = "path:../../";
};

outputs = inputs@{ flake-parts, landrun-nix, ... }:
Expand All @@ -18,6 +18,7 @@
};

landrunApps.default = {
name = "claude";
imports = [
landrun-nix.landrunModules.gh # So, Claude can run `gh` CLI
landrun-nix.landrunModules.git # So, Claude can run `git` CLI
Expand Down
12 changes: 12 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Run the claude-sandboxed example with local landrun-nix override
run-example:
nix run ./examples/claude-sandboxed --override-input landrun-nix .

# Run integration tests
test:
#!/usr/bin/env bash
if [ "$(uname)" = "Darwin" ]; then
# macOS: BSD script syntax: script -q <output> <command>
SCRIPT_ARGS="-q /dev/null ./tests/test.bats"
else
# Linux: util-linux script syntax: script -qec <command> <output>
SCRIPT_ARGS="-qec ./tests/test.bats /dev/null"
fi
nix develop ./tests --override-input landrun-nix path:./. -c script $SCRIPT_ARGS
63 changes: 63 additions & 0 deletions modules/flake-parts/landrun/common/features.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{ lib, config, ... }:
{
config = {
# Auto-configure CLI options based on high-level flags
cli = lib.mkMerge [
# TTY support
(lib.mkIf config.features.tty {
rw = [
"/dev/null"
"/dev/tty"
];
rox = [
"/dev/zero"
"/dev/random"
"/dev/urandom"
"/usr/share/terminfo"
];
env = [
"TERM"
"SHELL"
"COLORTERM"
"LANG"
"LC_ALL"
];
})

# Nix support
(lib.mkIf config.features.nix {
rox = [
"/nix"
"/usr"
"/lib"
];
rw = [
"$HOME/.cache/nix"
];
ro = [
"/etc/nix"
"$HOME/.local/share/nix"
];
env = [
"PATH" # Required for programs to find executables
"NIX_PATH"
"NIX_SSL_CERT_FILE"
];
})

# Network support
(lib.mkIf config.features.network {
rox = [
"/etc/resolv.conf"
"/etc/ssl"
];
unrestrictedNetwork = true;
})

# Tmp support
(lib.mkIf config.features.tmp {
rwx = [ "/tmp" ];
})
];
};
}
28 changes: 28 additions & 0 deletions modules/flake-parts/landrun/darwin/features.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{ lib, config, pkgs, ... }:
{
config = lib.mkIf pkgs.stdenv.isDarwin {
cli = lib.mkMerge [
# TTY support
(lib.mkIf config.features.tty {
rox = [
"/etc/profile" # Shell initialization
];
})

# Nix support
(lib.mkIf config.features.nix {
rox = [
"/bin"
];
ro = [
"/var/run/syslog" # Often needed for logging on macOS
];
})

# Tmp support
(lib.mkIf config.features.tmp {
rwx = [ "/var/folders" ];
})
];
};
}
157 changes: 157 additions & 0 deletions modules/flake-parts/landrun/darwin/wrapper.nix
Comment thread
adrian-gierakowski marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{ lib, config, pkgs, ... }:
let
pkg = pkgs.writeShellApplication {
name = config.name;
runtimeInputs = [ pkgs.coreutils ];
text = ''
user=''${USER:-nobody}
PROFILE_FILE=$(mktemp "/tmp/sandbox-profile-$user-XXXXXX.sb")
trap 'rm -f "$PROFILE_FILE"' EXIT

cat > "$PROFILE_FILE" <<EOF
;; Sandbox Profile Language (SBPL) version 1
(version 1)

;; Default deny policy: Everything is denied unless explicitly allowed
(deny default)

;; Allow forking child processes (essential for shell wrappers and most apps)
(allow process-fork)

;; Allow sending signals to processes
(allow signal)

;; Import standard system profile
;; This is required for basic system functionality:
;; - Dynamic linker (dyld) operation
;; - System frameworks and libraries (libc, libSystem, etc.)
;; - Basic kernel interactions (mach lookups, sysctls)
;; Without this, almost no binary can execute on macOS.
(import "system.sb")

;; Allow broad metadata access
;; 'file-read-metadata' is needed for:
;; - 'ls' (directory listing implies metadata read)
;; - 'getcwd' path resolution
;; - 'realpath' and symlink resolution
;; 'file-test-existence' is needed for:
;; - Library probing (dyld checking potential paths)
;; - Applications checking for optional config files
;; Note: This allows seeing *if* files exist, but not reading their content.
(allow file-read-metadata)
(allow file-test-existence)

;; Standard POSIX device nodes
;; Most applications expect these to be available.
(allow file-read*
(literal "/dev/null")
(literal "/dev/zero")
(literal "/dev/random")
(literal "/dev/urandom"))

${lib.optionalString config.features.tty ''
;; Allow PTY ioctls (enabled by features.tty)
;; Necessary for interactive applications (stty, shells, REPLs) to control the terminal.
(allow file-ioctl (regex #"^/dev/ttys[0-9]+"))
''}

;; Network Access Control
;; Based on 'features.network' or 'cli.unrestrictedNetwork'
${if config.cli.unrestrictedNetwork then "(allow network*)" else "(deny network*)"}

;; Filesystem Access Control
;; Based on 'cli.unrestrictedFilesystem'
${if config.cli.unrestrictedFilesystem then "(allow file*)" else ""}

EOF

# Isolation of environment variables (like landrun does)
ALLOWED_VARS=(${lib.concatStringsSep " " (map (e: "\"${e}\"") config.cli.env)})
KEEP_VARS=("HOME" "USER" "LOGNAME" "PATH" "TERM" "SHELL" "LANG" "LC_ALL" "DISPLAY")

ENV_ARGS=()
for var in "''${ALLOWED_VARS[@]}" "''${KEEP_VARS[@]}"; do
# Check if variable is set
if [[ -v "$var" ]]; then
ENV_ARGS+=("$var=''${!var}")
fi
done

# Function to add paths to SBPL profile
add_paths() {
local op=$1
shift
for p in "$@"; do
# Expand $HOME and $UID if present in path
p_expanded=''${p//\$HOME/$HOME}
p_expanded=''${p_expanded//\$UID/$(id -u)}

# Make path absolute if it's relative
if [[ "$p_expanded" != /* ]]; then
p_expanded="$(pwd -P)/$p_expanded"
fi

if [ -e "$p_expanded" ]; then
# Resolve to real path for macOS sandbox
p_real=$(${lib.getExe pkgs.perl} -e 'use Cwd "abs_path"; print abs_path(shift)' "$p_expanded")

case "$op" in
rox)
echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow process-exec (subpath \"$p_real\"))" >> "$PROFILE_FILE"
;;
ro)
echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
;;
rw)
echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow file-write* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow file-ioctl (subpath \"$p_real\"))" >> "$PROFILE_FILE"
;;
rwx)
echo "(allow file-read* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow file-write* (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow file-ioctl (subpath \"$p_real\"))" >> "$PROFILE_FILE"
echo "(allow process-exec (subpath \"$p_real\"))" >> "$PROFILE_FILE"
;;
esac
fi
done
}

# Fix getcwd by allowing traversal of parents
# Use physical path for CURR_PATH
CURR_PATH="$(pwd -P)"
while [ "$CURR_PATH" != "/" ]; do
echo "(allow file-read* (literal \"$CURR_PATH\"))" >> "$PROFILE_FILE"
CURR_PATH=$(dirname "$CURR_PATH")
done

${lib.optionalString config.cli.addExec ''
echo "(allow file-read* (literal \"${config.program}\"))" >> "$PROFILE_FILE"
echo "(allow process-exec (literal \"${config.program}\"))" >> "$PROFILE_FILE"
''}

# shellcheck disable=SC2016
${lib.optionalString (config.cli.rox != []) "add_paths rox ${lib.escapeShellArgs config.cli.rox}\n"}
# shellcheck disable=SC2016
${lib.optionalString (config.cli.ro != []) "add_paths ro ${lib.escapeShellArgs config.cli.ro}\n"}
# shellcheck disable=SC2016
${lib.optionalString (config.cli.rw != []) "add_paths rw ${lib.escapeShellArgs config.cli.rw}\n"}
# shellcheck disable=SC2016
${lib.optionalString (config.cli.rwx != []) "add_paths rwx ${lib.escapeShellArgs config.cli.rwx}\n"}

# Execute with isolated environment
exec env -i "''${ENV_ARGS[@]}" sandbox-exec -f "$PROFILE_FILE" ${config.program} "$@"
'';
};
in
{
config = lib.mkIf pkgs.stdenv.isDarwin {
wrappedPackage =
if config.cli.extraArgs != [ ] then
lib.warn "landrun-nix: extraArgs are ignored on Darwin as sandbox-exec does not support them." pkg
else
pkg;
};
}
2 changes: 1 addition & 1 deletion modules/flake-parts/landrun/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ in
apps = lib.mapAttrs
(name: cfg: {
type = "app";
program = "${cfg.wrappedPackage}/bin/${name}";
program = lib.getExe cfg.wrappedPackage;
meta = cfg.meta;
})
config.landrunApps;
Expand Down
Loading