From 2f4c5fa6c28187695b3a977ef025e3bff8aa7961 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 20 May 2026 04:23:59 +0100 Subject: [PATCH 1/4] [flake.lock] --- flake.lock | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..75d7b51 --- /dev/null +++ b/flake.lock @@ -0,0 +1,41 @@ +{ + "nodes": { + "jail-nix": { + "locked": { + "lastModified": 1772137954, + "narHash": "sha256-h4MGNbOo7L3RHi4uNFmsg5g17/DHXEfnv/xiG6BrNFQ=", + "owner": "~alexdavid", + "repo": "jail.nix", + "rev": "42b355c38ca63dab4904acc5c0d95f17954a8c9b", + "type": "sourcehut" + }, + "original": { + "owner": "~alexdavid", + "repo": "jail.nix", + "type": "sourcehut" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "jail-nix": "jail-nix", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} From f93f12cfe6a8dc5278563aa96874a7f01d69f996 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 20 May 2026 04:24:37 +0100 Subject: [PATCH 2/4] fix[tests]: fixed the `checks` output --- flake.nix | 4 ++-- tests.nix | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 6826446..4254473 100644 --- a/flake.nix +++ b/flake.nix @@ -163,7 +163,7 @@ system: let pkgs = nixpkgs.legacyPackages.${system}; - test-names = builtins.attrNames self.checks.${system}.test; + test-names = builtins.attrNames (builtins.removeAttrs self.checks.${system} [ "all" ]); in { test-logs = { @@ -173,7 +173,7 @@ failed=0 for test in ${builtins.concatStringsSep " " test-names}; do echo "=== $test ===" - if nix build ".#checks.${system}.test.$test" -L --no-link 2>&1; then + if nix build ".#checks.${system}.$test" -L --no-link 2>&1; then echo "=== PASS: $test ===" else echo "=== FAIL: $test ===" diff --git a/tests.nix b/tests.nix index c91064e..e791ba9 100644 --- a/tests.nix +++ b/tests.nix @@ -382,8 +382,5 @@ nixpkgs.lib.genAttrs ''; in - { - inherit all; - test = tests; - } + tests // { inherit all; } ) From 4d5033b00141384ad683c10524896011667cd216 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 20 May 2026 05:05:31 +0100 Subject: [PATCH 3/4] ench: simplified `NIX_CONFIG` appending my assumption here is -- nix doesn't care about the newlines in its config, so I would prefer it to cope with a double newline, as it drastically simplifies the `NIX_CONFIG` handling: - no obscure append guard - no double escaped newlines - shorter code lines in general tag: deslop --- flake.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4254473..21edc09 100644 --- a/flake.nix +++ b/flake.nix @@ -146,7 +146,12 @@ SOCAT_PID=$! trap "${pkgs.coreutils}/bin/rm -rf $JD; ${pkgs.coreutils}/bin/kill $PROXY_PID $SOCAT_PID" EXIT - export NIX_CONFIG="''${NIX_CONFIG:+$NIX_CONFIG$'\n'}extra-experimental-features = nix-command flakes"$'\n'"substitute = true" + + export NIX_CONFIG=" + ''${NIX_CONFIG:-} + extra-experimental-features = nix-command flakes + substitute = true" + ${jailed}/bin/unsafe-cell ''; in From 5ec64acd06d1f602be882bbbbb2adbdd697ea990 Mon Sep 17 00:00:00 2001 From: Dzuchun Date: Wed, 20 May 2026 05:20:37 +0100 Subject: [PATCH 4/4] fix[docs]: clarify framing of the tool project - correct tool positioning -- explain intended use cases without getting into too much detail - some fact-checking -- shared proc and IPC namespaces claim is totall bogus - document the tools this flake is based on, and explain the exposure surface for each - remove some explanations that were relevant in a context of the old flake version -- old version is not published and will not be observable to the user, so it must not be mentioned - more laid-back and grounded documentation style: the intended audience are engineers like myself -- not an LLM, not an investor tag: deslop --- README.md | 71 ++++++++++++++++++++++++++++--------------------------- flake.nix | 2 +- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 79df4b7..0fa8a51 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,50 @@ -# unsafe-cell +# Brief -A `jail-nix` container that shares user namespace and grants all capabilities (the "unsafe" part), packaging a command into a self-contained executable ("cell"). It provides nix daemon passthrough, configurable networking, and filesystem isolation — enough to run agentic workloads without the friction of full containerization. +`unsafe-cell` is designed with following workloads in mind: +- share a cache/binary store with the host +- share a configuration with the host +- allow limited internet access (to a set list of domains) +- jailed program might misbehave -- host must be protected from _accidental_ damage -## Capabilities +So this cell aims to prevent malfunction, not malice; for example: current design shares a nix daemon with the host -- a malicious code could easily exploit the build pipeline to dos, mine crypto or circumvent the web proxy. However, this kind of malfunction is extremely unlikely happen _accidentally_, while benefits of sharing the nix store with host are self-explanatory. -Shares user/id mappings 1:1 with the host. All kernel capabilities granted. No PID namespace isolation — host PID 1 visible inside. New mount namespace with only explicitly bound paths. - -Nix daemon access via read-only bind of host store (`/nix/store`) and daemon socket (`/run/nix`). Nix config and experimental features (`nix-command`, `flakes`) set inside the jail. Binary substitution enabled. - -Network access is always proxied through tinyproxy with an isolated network namespace (no network routes inside the jail). A socat pair forwards TCP through a Unix socket onto tinyproxy. An optional tinyproxy filter file controls allowed domains; defaults to allow-all (`.*`). - -Working directory bind-mounted at a configurable host path. Optional log directory bind. - -Entrypoint runs the command derivation supplied via the `command` parameter. `PATH` includes `nix`, `coreutils`. +Example use-cases: +- trusted CI job container +- agentic runtime (like `opencode`, for instance) ## Parameters -`command` (`pkgs -> derivation`): Builds the jailed entrypoint. Receives nixpkgs. +- `command` (`pkgs -> derivation`): a function to build the cell entrypoint from `nixpkgs` instantiation +- `worktreePath` (string): host path to bind as `/home/cell/worktree` inside the jail. Use this parameter to expose jailed processes primary worktree (like a repo you work on) +- `extraCombinators` (`jail-nix -> list`, default `_: []`): a function to produce additional [`jail.nix` combinators](https://alexdav.id/projects/jail-nix/combinators) you want the jail to have. +- `tinyproxyFilter` (string or null): Path to a tinyproxy filter file. `null` means "allow all", non-`null` should contain a regex list of the allowed domains. Uses [`tinyproxy`](https://tinyproxy.github.io/) (with your filter as a white-list!). +- `logDir` (string or null): directory to bind for log output. `null` means using the jail tempdir (wherever `mktemp` creates it). +- `nixDaemonSocket` (string, default `"/var/nix/daemon-socket"`): host path to nix daemon socket directory (contains `socket`). -`worktreePath` (string): Host path to bind as `/home/cell/worktree` inside the jail. Required, no default. +NOTE: if you want to pass some truly special arguments to `bwrap`, see `unsafe-add-raw-args` jail.nix combinator -- an escape hatch for adding bare os args for the `bwrap` process. +NOTE: for now, the host is expected to have a nix daemon socket. To make the cell work, at the bare minimum, you should make up a socket and point `nixDaemonSocket` to it -- cell will connect `socat` to it, but won't actually communicate with the other side, for as long as you don't use `nix` inside the jail. -`extraCombinators` (`jail-nix -> list`, default `_: []`): jail-nix combinators appended to the default set. +## Capabilities -`tinyproxyFilter` (string or null): Path to a tinyproxy filter file. `null` = default allow-all filter (`.*`); non-null = use the specified filter file. +- nix daemon access: + - read-only bind of host store (`/nix/store`) + - bind of the daemon socket (`/run/nix`) + - nix client config to silence experimental feature warnings (`nix-command`, `flakes`) + - nix client substitutions enabled to allow binary cache usage (client controls that, for some reason?) +- network: + - http proxied through `tinyproxy`, with optional filter + - isolated network namespace with no interfaces +- workdir bind-mounted at a configurable host path +- optional log directory bind -`logDir` (string or null): Directory to bind for log output. `null` = use jail tempdir (`$JD`). +planned: +- spawning a personal dockerd (blocked by some annoying stuff inside `bwrap`) +- support for MITM docker image cache (could be implemented now, but is useless without a personal dockerd) -`nixDaemonSocket` (string, default `"/var/nix/daemon-socket"`): Host path to nix daemon socket directory (contains `socket`). +NOTE: unfortunately, outward communication with protocols other than HTTP(S) is not supported. Any celled app failing to interpret `{HTTP{,S},NO}_PROXY` envvars, is also likely to misbehave. -## Implicit assumptions +## Usage example -`mkCell` builds its output for every system in `systems.flakeExposed`. Requires `nixpkgs` and `jail-nix` as flake inputs. +See `tests.nix`. -The nix daemon socket path is assumed to be a **directory** containing a `socket` entry — the `NIX_REMOTE` env var always resolves `/run/nix/socket` regardless of `nixDaemonSocket`. - -`/nix/store` is assumed present and readable on the host. `NIX_PATH` hardcoded to `/home/cell/.nix-defexpr/channels`. - -`NIX_CONFIG` unconditionally overrides host nix config — forces `nix-command`, `flakes`, and `substitute = true`. - -Host-side packages required at nixpkgs evaluation time: `socat`, `coreutils`, `nix`, `iproute2`, `gawk`, `tinyproxy`. - -`worktreePath` is resolved relative to the host-script's CWD at runtime (shell expansion applies). - -The default combinator set does not include SSL certificates, app config/state directories, nix registry mapping, nix config files, or secret tokens — these must be supplied by the consumer via `extraCombinators`. - -The host script always creates `$JD` (temp dir) with tinyproxy config, SSL overlay dirs, and Unix socket — all cleaned up on exit. The SSL overlay dirs are created but unused (dead code from original hardcoded version). - -No IPC namespace isolation — inherits from host. +Will probably add my `opencode` run script, once it's complete. diff --git a/flake.nix b/flake.nix index 21edc09..7d1d4c6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "unsafe-cell - isolated environment with trusted nix store access"; + description = "unsafe-cell - a reasonably-isolated environment"; inputs = { nixpkgs.url = "nixpkgs";